commit
4a5a093a19
|
@ -46,6 +46,7 @@
|
|||
"graphql": "^16.7.1",
|
||||
"graphql-request": "^6.0.0",
|
||||
"next": "13.3.0",
|
||||
"next-query-params": "^4.2.3",
|
||||
"nextjs-progressbar": "^0.0.16",
|
||||
"react": "18.2.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
|
@ -53,7 +54,8 @@
|
|||
"react-imgix": "^9.7.0",
|
||||
"react-player": "^2.12.0",
|
||||
"react-use": "^17.4.0",
|
||||
"typescript": "5.0.4"
|
||||
"typescript": "5.0.4",
|
||||
"use-query-params": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
|
|
|
@ -3,6 +3,7 @@ import styled from '@emotion/styled'
|
|||
import { PostCard } from '@/components/PostCard'
|
||||
import { SearchResultTopPostBlocks } from '@/components/Search/SearchResult.TopPost.Blocks'
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
import { lsdUtils } from '@/utils/lsd.utils'
|
||||
|
||||
interface Props {
|
||||
post: LPE.Search.ResultItemBase<LPE.Post.Document>
|
||||
|
@ -29,4 +30,8 @@ export const SearchResultTopPost = ({ post, shows, blocks }: Props) => {
|
|||
|
||||
const Container = styled.div`
|
||||
padding: 24px 0;
|
||||
|
||||
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
|
||||
padding: 0;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,40 +1,29 @@
|
|||
import styled from '@emotion/styled'
|
||||
import styles from '@/components/Searchbar/Search.module.css'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
TabItem,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@acid-info/lsd-react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
addContentTypesToQuery,
|
||||
addQueryToQuery,
|
||||
addTopicsToQuery,
|
||||
extractContentTypesFromQuery,
|
||||
extractQueryFromQuery,
|
||||
extractTopicsFromQuery,
|
||||
} from '@/utils/search.utils'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { Dropdown, TabItem, Tabs, Typography } from '@acid-info/lsd-react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { nope } from '@/utils/general.utils'
|
||||
import { LPE } from '@/types/lpe.types'
|
||||
import PostTypes = LPE.PostTypes
|
||||
import ContentBlockTypes = LPE.Post.ContentBlockTypes
|
||||
import { ESearchScope } from '@/types/ui.types'
|
||||
import { copyConfigs } from '@/configs/copy.configs'
|
||||
import { useOutsideClick, useSticky } from '@/utils/ui.utils'
|
||||
import { uiConfigs } from '@/configs/ui.configs'
|
||||
|
||||
import { useHydrated } from '@/utils/useHydrated.util'
|
||||
import {
|
||||
ArrayParam,
|
||||
StringParam,
|
||||
useQueryParam,
|
||||
withDefault,
|
||||
} from 'use-query-params'
|
||||
import { useDeepCompareEffect } from 'react-use'
|
||||
import { lsdUtils } from '@/utils/lsd.utils'
|
||||
interface SearchBoxProps {
|
||||
onSearch?: (query: string, tags: string[], types: LPE.ContentType[]) => void
|
||||
tags?: string[]
|
||||
onViewChange?: (view: string) => void
|
||||
resultsNumber: number | null
|
||||
busy?: boolean
|
||||
showModeSwitch?: boolean
|
||||
}
|
||||
|
||||
const ContentTypesCategories = {
|
||||
|
@ -67,83 +56,6 @@ const contentTypes = [
|
|||
|
||||
const allContentTypes = contentTypes.map((c) => c.value)
|
||||
|
||||
const useSearchBox = (
|
||||
router: NextRouter,
|
||||
callback: (query: string, tags: string[], types: LPE.ContentType[]) => void,
|
||||
) => {
|
||||
const [query, setQuery] = useState<string>(
|
||||
extractQueryFromQuery(router.query),
|
||||
)
|
||||
const [filterTags, setFilterTags] = useState<string[]>(
|
||||
extractTopicsFromQuery(router.query),
|
||||
)
|
||||
const [filterContentTypes, setFilterContentTypes] =
|
||||
useState<string[]>(allContentTypes)
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(extractQueryFromQuery(router.query))
|
||||
setFilterTags(extractTopicsFromQuery(router.query))
|
||||
const contentTypes = extractContentTypesFromQuery(router.query)
|
||||
setFilterContentTypes(contentTypes.length ? contentTypes : allContentTypes)
|
||||
}, [router.query, router.query.topics, router.pathname])
|
||||
|
||||
const performSearch = useCallback(
|
||||
async (
|
||||
q: string = query,
|
||||
_filterTags: string[] = filterTags,
|
||||
_contentTypes: string[] = [],
|
||||
) => {
|
||||
const queries = [
|
||||
addQueryToQuery(q),
|
||||
addTopicsToQuery(_filterTags),
|
||||
addContentTypesToQuery(
|
||||
_contentTypes.length === allContentTypes.length ? [] : _contentTypes,
|
||||
),
|
||||
].filter((n) => n && n)
|
||||
|
||||
callback(q, _filterTags, _contentTypes as LPE.ContentType[])
|
||||
|
||||
await router.push(
|
||||
{
|
||||
pathname: '/search',
|
||||
query: queries.length ? queries.join('&') : undefined,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
shallow: true,
|
||||
},
|
||||
)
|
||||
},
|
||||
[router, filterTags, filterContentTypes, query],
|
||||
)
|
||||
|
||||
const handleEnter = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
await performSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypesChange = async (n: string | string[]) => {
|
||||
await performSearch(undefined, undefined, n as string[])
|
||||
}
|
||||
const handleTagsChange = async (n: string | string[]) => {
|
||||
await performSearch(undefined, n as string[])
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
filterTags,
|
||||
filterContentTypes,
|
||||
setQuery,
|
||||
setFilterTags,
|
||||
setFilterContentTypes,
|
||||
handleEnter,
|
||||
handleTypesChange,
|
||||
handleTagsChange,
|
||||
performSearch,
|
||||
}
|
||||
}
|
||||
|
||||
const SearchBox = (props: SearchBoxProps) => {
|
||||
const {
|
||||
onSearch = nope,
|
||||
|
@ -151,20 +63,30 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
tags = [],
|
||||
resultsNumber,
|
||||
busy = false,
|
||||
showModeSwitch = true,
|
||||
} = props
|
||||
const router = useRouter()
|
||||
const {
|
||||
query,
|
||||
filterTags,
|
||||
filterContentTypes,
|
||||
setQuery,
|
||||
handleEnter,
|
||||
handleTypesChange,
|
||||
handleTagsChange,
|
||||
performSearch,
|
||||
} = useSearchBox(router, onSearch)
|
||||
const [view, setView] = useState<string>('list')
|
||||
|
||||
const [filterTags, setFilterTags] = useQueryParam(
|
||||
'topic',
|
||||
withDefault(ArrayParam, []),
|
||||
{
|
||||
skipUpdateWhenNoChange: false,
|
||||
},
|
||||
)
|
||||
const [filterContentTypes, setFilterContentTypes] = useQueryParam(
|
||||
'type',
|
||||
withDefault(ArrayParam, allContentTypes),
|
||||
{
|
||||
skipUpdateWhenNoChange: true,
|
||||
},
|
||||
)
|
||||
const [query, setQuery] = useQueryParam('q', withDefault(StringParam, ''), {
|
||||
skipUpdateWhenNoChange: true,
|
||||
})
|
||||
|
||||
const [queryInput, setQueryInput] = useState<string>(query)
|
||||
|
||||
const [view, setView] = useState<string>('list')
|
||||
const [enlargeQuery, setEnlargeQuery] = useState(false)
|
||||
const [placeholder, setPlaceholder] = useState<string>(
|
||||
copyConfigs.search.searchbarPlaceholders.global(),
|
||||
|
@ -185,18 +107,24 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
const handleViewChange = async (n: string) => {
|
||||
setView(n)
|
||||
onViewChange(n)
|
||||
performSearch()
|
||||
onSearch(
|
||||
query,
|
||||
filterTags as string[],
|
||||
filterContentTypes as LPE.ContentType[],
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (enlargeQuery) {
|
||||
setEnlargeQuery(!(queryInput.length === 0 && !focused))
|
||||
if (focused) {
|
||||
setPlaceholder('')
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setPlaceholder(copyConfigs.search.searchbarPlaceholders.global())
|
||||
}, 200)
|
||||
setQuery('')
|
||||
}
|
||||
}, [enlargeQuery])
|
||||
}, [focused, queryInput])
|
||||
|
||||
useEffect(() => {
|
||||
if (filtersRef.current) {
|
||||
|
@ -204,7 +132,7 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
const parentT =
|
||||
filtersRef.current.parentElement?.getBoundingClientRect().top || 0
|
||||
const whereResultsStick =
|
||||
-1 * (filtersB - parentT - uiConfigs.navbarRenderedHeight)
|
||||
-1 * (filtersB - parentT - uiConfigs.navbarRenderedHeight + 2)
|
||||
setWhereResultsStick(whereResultsStick)
|
||||
setDetailsTop(filtersB + whereResultsStick)
|
||||
}
|
||||
|
@ -218,17 +146,6 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [detailsTop])
|
||||
|
||||
useEffect(() => {
|
||||
if (query.length === 0 && mounted && !focused) {
|
||||
setTimeout(() => {
|
||||
setEnlargeQuery(false)
|
||||
}, 200)
|
||||
} else {
|
||||
setEnlargeQuery(true)
|
||||
setPlaceholder('')
|
||||
}
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
filterContentTypes.length < allContentTypes.length ||
|
||||
|
@ -240,10 +157,32 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
}
|
||||
}, [filterTags, filterContentTypes])
|
||||
|
||||
const handleClear = async () => {
|
||||
await performSearch(undefined, [], [])
|
||||
const clear = async () => {
|
||||
setQuery('')
|
||||
setFilterTags([])
|
||||
setFilterContentTypes(allContentTypes)
|
||||
}
|
||||
|
||||
const handleKeyUp = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
setQuery(queryInput)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (query?.length > 0) setQueryInput(query)
|
||||
}, [query])
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
onSearch(
|
||||
query,
|
||||
filterTags as string[],
|
||||
filterContentTypes as LPE.ContentType[],
|
||||
)
|
||||
}, [query, filterTags, filterContentTypes])
|
||||
|
||||
const hydrated = useHydrated()
|
||||
|
||||
return (
|
||||
<Container
|
||||
className={enlargeQuery ? 'active' : ''}
|
||||
|
@ -253,54 +192,51 @@ const SearchBox = (props: SearchBoxProps) => {
|
|||
<input
|
||||
className={`search-input`}
|
||||
placeholder={placeholder}
|
||||
value={query as string}
|
||||
value={queryInput as string}
|
||||
onFocus={() => {
|
||||
if (query.length === 0) {
|
||||
setEnlargeQuery(true)
|
||||
}
|
||||
setFocused(true)
|
||||
}}
|
||||
onKeyDown={handleEnter}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={(e) => setQueryInput(e.target.value)}
|
||||
onBlurCapture={() => {
|
||||
if (query.length === 0) {
|
||||
setEnlargeQuery(false)
|
||||
performSearch()
|
||||
}
|
||||
setFocused(false)
|
||||
}}
|
||||
/>
|
||||
<ViewButtons>
|
||||
<Tabs size={'small'} activeTab={view} onChange={handleViewChange}>
|
||||
<TabItem name={'list'}>{copyConfigs.search.views.default}</TabItem>
|
||||
<TabItem name={'explore'}>
|
||||
{copyConfigs.search.views.explore}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</ViewButtons>
|
||||
{showModeSwitch && (
|
||||
<ViewButtons>
|
||||
<Tabs size={'small'} activeTab={view} onChange={handleViewChange}>
|
||||
<TabItem name={'list'}>
|
||||
{copyConfigs.search.views.default}
|
||||
</TabItem>
|
||||
<TabItem name={'explore'}>
|
||||
{copyConfigs.search.views.explore}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</ViewButtons>
|
||||
)}
|
||||
</FirstRow>
|
||||
<Filters ref={filtersRef}>
|
||||
<Dropdown
|
||||
size={'small'}
|
||||
placeholder={'Content Type'}
|
||||
options={contentTypes.map((c) => ({ name: c.label, value: c.value }))}
|
||||
value={filterContentTypes}
|
||||
onChange={handleTypesChange}
|
||||
value={hydrated ? (filterContentTypes as string[]) : []}
|
||||
onChange={(n) => setFilterContentTypes(n as string[])}
|
||||
multi={true}
|
||||
/>
|
||||
<Dropdown
|
||||
size={'small'}
|
||||
placeholder={'Topics'}
|
||||
options={tags.map((t) => ({ name: t, value: t }))}
|
||||
value={filterTags}
|
||||
onChange={handleTagsChange}
|
||||
value={hydrated ? (filterTags as string[]) : []}
|
||||
onChange={(n) => setFilterTags(n as string[])}
|
||||
multi={true}
|
||||
triggerLabel={'Topics'}
|
||||
/>
|
||||
<Clear
|
||||
variant={'label2'}
|
||||
className={`${showClear ? 'show' : ''}`}
|
||||
onClick={handleClear}
|
||||
onClick={clear}
|
||||
>
|
||||
Clear Filters
|
||||
</Clear>
|
||||
|
@ -352,7 +288,7 @@ const Container = styled.div`
|
|||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(var(--lsd-text-primary), 1);
|
||||
padding: 8px 0 14px 14px;
|
||||
padding: 8px 14px;
|
||||
position: sticky;
|
||||
|
||||
z-index: 1;
|
||||
|
@ -391,7 +327,6 @@ const Container = styled.div`
|
|||
}
|
||||
`
|
||||
|
||||
// align last item to the right and first item takes the rest of the space
|
||||
const FirstRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -410,6 +345,12 @@ const Filters = styled.div`
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
|
||||
.lsd-dropdown--small {
|
||||
width: 135px;
|
||||
}
|
||||
}
|
||||
`
|
||||
const Results = styled.div`
|
||||
width: 100%;
|
||||
|
@ -420,6 +361,10 @@ const Results = styled.div`
|
|||
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
|
|
|
@ -5,4 +5,9 @@ export const uiConfigs = {
|
|||
articleSectionMargin: 40,
|
||||
maxContainerWidth: 1440,
|
||||
articleRenderedMT: 45 * 2,
|
||||
searchExplore: {
|
||||
gridColumns: 16,
|
||||
largeColumn: 4,
|
||||
smallColumn: 2,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,13 +4,76 @@ import { Grid, GridItem } from '@/components/Grid/Grid'
|
|||
import { useMemo } from 'react'
|
||||
import { PostCard } from '@/components/PostCard'
|
||||
import { ImageBlock, TextBlock } from '@/components/ContentBlock'
|
||||
import { uiConfigs } from '@/configs/ui.configs'
|
||||
|
||||
type Block = LPE.Search.ResultItemBase<LPE.Post.ContentBlock>
|
||||
type Post = LPE.Search.ResultItemBase<LPE.Post.Document>
|
||||
|
||||
interface Props {
|
||||
posts: LPE.Search.ResultItemBase<LPE.Post.Document>[]
|
||||
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
|
||||
posts: Post[]
|
||||
blocks: Block[]
|
||||
shows: LPE.Podcast.Show[]
|
||||
}
|
||||
|
||||
const distributeBlocks = (blocks: (Block | Post)[]): (Block | Post)[][] => {
|
||||
let rows: (Block | Post)[][] = []
|
||||
let currentRow: (Block | Post)[] = []
|
||||
let currentRowWidth = 0
|
||||
|
||||
const getBlockWidth = (block: Block | Post): number => {
|
||||
return block.type === LPE.ContentTypes.Image
|
||||
? uiConfigs.searchExplore.smallColumn
|
||||
: uiConfigs.searchExplore.largeColumn
|
||||
}
|
||||
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i]
|
||||
const blockWidth = getBlockWidth(block)
|
||||
|
||||
if (currentRowWidth + blockWidth <= uiConfigs.searchExplore.gridColumns) {
|
||||
currentRow.push(block)
|
||||
currentRowWidth += blockWidth
|
||||
} else {
|
||||
// Look ahead to find a block that fits.
|
||||
let swapIndex = -1
|
||||
for (let j = i + 1; j < blocks.length; j++) {
|
||||
const nextBlockWidth = getBlockWidth(blocks[j])
|
||||
if (
|
||||
currentRowWidth + nextBlockWidth <=
|
||||
uiConfigs.searchExplore.gridColumns
|
||||
) {
|
||||
swapIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (swapIndex !== -1) {
|
||||
// Swap blocks and add the fitting block to the current row.
|
||||
;[blocks[i], blocks[swapIndex]] = [blocks[swapIndex], blocks[i]]
|
||||
currentRow.push(blocks[i])
|
||||
currentRowWidth += blockWidth
|
||||
} else {
|
||||
// No fitting block found, go with the best fit.
|
||||
rows.push([...currentRow])
|
||||
currentRow = [block]
|
||||
currentRowWidth = blockWidth
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRowWidth === uiConfigs.searchExplore.gridColumns) {
|
||||
rows.push([...currentRow])
|
||||
currentRow = []
|
||||
currentRowWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow.length) {
|
||||
rows.push(currentRow)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export const SearchResultsExploreView = (props: Props) => {
|
||||
const { posts, shows, blocks } = props
|
||||
const results = useMemo(() => {
|
||||
|
@ -18,50 +81,88 @@ export const SearchResultsExploreView = (props: Props) => {
|
|||
return b.score - a.score
|
||||
})
|
||||
}, [posts, blocks])
|
||||
|
||||
const items = distributeBlocks(results)
|
||||
|
||||
return (
|
||||
<Container cols={12}>
|
||||
{results.map((result, index) => {
|
||||
return (
|
||||
<ResultItem
|
||||
key={index}
|
||||
className={result.type === LPE.ContentTypes.Image ? 'w-2' : 'w-4'}
|
||||
>
|
||||
{(() => {
|
||||
switch (result.type) {
|
||||
case LPE.ContentTypes.Article:
|
||||
case LPE.ContentTypes.Podcast:
|
||||
return (
|
||||
<PostCard
|
||||
data={PostCard.toData(
|
||||
result.data as LPE.Post.Document,
|
||||
shows,
|
||||
)}
|
||||
size={'medium'}
|
||||
contentType={result.type as LPE.PostType}
|
||||
/>
|
||||
)
|
||||
case LPE.ContentTypes.Image:
|
||||
return (
|
||||
<ImageBlock
|
||||
{...(result as LPE.Search.ResultItemBase<LPE.Post.ImageBlock>)}
|
||||
/>
|
||||
)
|
||||
case LPE.ContentTypes.Text:
|
||||
return (
|
||||
<TextBlock
|
||||
{...(result as LPE.Search.ResultItemBase<LPE.Post.TextBlock>)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
</ResultItem>
|
||||
)
|
||||
})}
|
||||
<Container>
|
||||
{items.map((row, idx) => (
|
||||
<Row key={`search-result-row-${idx}`} cols={12}>
|
||||
{row.map((result, index) => {
|
||||
return (
|
||||
<ResultItem
|
||||
key={index}
|
||||
className={
|
||||
result.type === LPE.ContentTypes.Image ? 'w-2' : 'w-4'
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
switch (result.type) {
|
||||
case LPE.ContentTypes.Podcast:
|
||||
return (
|
||||
<PostCard
|
||||
data={PostCard.toData(
|
||||
result.data as LPE.Post.Document,
|
||||
shows,
|
||||
)}
|
||||
size={'medium'}
|
||||
contentType={result.type as LPE.PostType}
|
||||
/>
|
||||
)
|
||||
case LPE.ContentTypes.Article:
|
||||
return (
|
||||
<PostCard
|
||||
data={{
|
||||
...PostCard.toData(
|
||||
result.data as LPE.Post.Document,
|
||||
shows,
|
||||
),
|
||||
coverImage: null,
|
||||
}}
|
||||
size={'medium'}
|
||||
contentType={result.type as LPE.PostType}
|
||||
/>
|
||||
)
|
||||
case LPE.ContentTypes.Image:
|
||||
return (
|
||||
<ImageBlock
|
||||
{...(result as LPE.Search.ResultItemBase<LPE.Post.ImageBlock>)}
|
||||
/>
|
||||
)
|
||||
case LPE.ContentTypes.Text:
|
||||
return (
|
||||
<TextBlock
|
||||
{...(result as LPE.Search.ResultItemBase<LPE.Post.TextBlock>)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
</ResultItem>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(Grid)``
|
||||
const ResultItem = styled(GridItem)``
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 56px;
|
||||
`
|
||||
const Row = styled(Grid)`
|
||||
grid-template-columns: repeat(${uiConfigs.searchExplore.gridColumns}, 1fr);
|
||||
grid-column-gap: 16px;
|
||||
`
|
||||
const ResultItem = styled(GridItem)`
|
||||
border-top: 1px solid rgb(var(--lsd-text-primary));
|
||||
padding: 24px 0;
|
||||
|
||||
> * {
|
||||
padding-top: 0;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -7,20 +7,18 @@ import { SearchResultListPosts } from '@/components/Search/SearchResultList.Post
|
|||
import { useMemo } from 'react'
|
||||
import { SearchResultTopPost } from '@/components/Search/SearchResult.TopPost'
|
||||
import { SearchResultListBlocks } from '@/components/Search/SearchResult.Blocks'
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
import { lsdUtils } from '@/utils/lsd.utils'
|
||||
|
||||
interface Props {
|
||||
posts: LPE.Search.ResultItemBase<LPE.Post.Document>[]
|
||||
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
|
||||
shows: LPE.Podcast.Show[]
|
||||
}
|
||||
|
||||
type Accumulator = {
|
||||
count: number
|
||||
block: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>
|
||||
busy: boolean
|
||||
}
|
||||
|
||||
export const SearchResultsListView = (props: Props) => {
|
||||
const { posts, shows, blocks } = props
|
||||
const { posts, shows, blocks, busy } = props
|
||||
|
||||
const mostReferredPostIndex = useMemo(() => {
|
||||
// Extract the IDs of the first 3 posts
|
||||
|
@ -90,25 +88,37 @@ export const SearchResultsListView = (props: Props) => {
|
|||
</PostsListHeader>
|
||||
)}
|
||||
<PostsListContent>
|
||||
{renderPosts.length > 0 && (
|
||||
{renderPosts.length > 0 ? (
|
||||
<>
|
||||
<SearchResultsListHeader
|
||||
title={copyConfigs.search.labels.articlesAndPodcasts}
|
||||
/>
|
||||
<SearchResultListPosts posts={renderPosts} shows={shows} />
|
||||
</>
|
||||
) : (
|
||||
!busy && (
|
||||
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
|
||||
No results found
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</PostsListContent>
|
||||
</PostsList>
|
||||
<GridItem className={'w-1'} />
|
||||
<BlocksList className={'w-3'}>
|
||||
{renderBlocks.length > 0 && (
|
||||
{renderBlocks.length > 0 ? (
|
||||
<>
|
||||
<SearchResultsListHeader
|
||||
title={copyConfigs.search.labels.relatedContent}
|
||||
/>
|
||||
<SearchResultListBlocks blocks={renderBlocks} />
|
||||
</>
|
||||
) : (
|
||||
!busy && (
|
||||
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
|
||||
No related content found
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</BlocksList>
|
||||
</Container>
|
||||
|
@ -117,12 +127,18 @@ export const SearchResultsListView = (props: Props) => {
|
|||
|
||||
const Container = styled(Grid)`
|
||||
padding-top: 56px;
|
||||
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
|
||||
padding-top: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const PostsList = styled(GridItem)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 56px;
|
||||
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
|
||||
gap: 32px;
|
||||
}
|
||||
`
|
||||
const PostsListHeader = styled.div``
|
||||
const PostsListContent = styled.div``
|
||||
|
|
|
@ -2,11 +2,19 @@ import { SearchBox } from '@/components/SearchBox'
|
|||
import { SearchResultsExploreView } from '@/containers/Search/ExploreView'
|
||||
import { SearchResultsListView } from '@/containers/Search/ListView'
|
||||
import { LPE } from '@/types/lpe.types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import SEO from '../components/SEO/SEO'
|
||||
import { DefaultLayout } from '../layouts/DefaultLayout'
|
||||
import { api } from '../services/api.service'
|
||||
import unbodyApi from '../services/unbody/unbody.service'
|
||||
import { QueryParamProvider } from 'use-query-params'
|
||||
import NextAdapterPages from 'next-query-params'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { seq } from 'yaml/dist/schema/common/seq'
|
||||
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'
|
||||
|
||||
interface SearchPageProps {
|
||||
topics: string[]
|
||||
|
@ -18,15 +26,8 @@ interface SearchPageProps {
|
|||
export default function SearchPage({ topics, shows }: SearchPageProps) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [resultNumber, setResultNumber] = useState<number | null>(null)
|
||||
const [view, setView] = useState<string>('list')
|
||||
|
||||
const [blocks, setBlocks] = useState<
|
||||
LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
|
||||
>([])
|
||||
const [posts, setPosts] = useState<
|
||||
LPE.Search.ResultItemBase<LPE.Post.Document>[]
|
||||
>([])
|
||||
const isMobile = useWindowSize().width < 768
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
@ -35,24 +36,38 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const [query, setQuery] = useState<string>('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
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[],
|
||||
})
|
||||
})
|
||||
|
||||
const blocks = (data?.data.blocks ||
|
||||
[]) as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
|
||||
const posts = (data?.data.posts ||
|
||||
[]) as LPE.Search.ResultItemBase<LPE.Post.Document>[]
|
||||
const handleSearch = async (
|
||||
query: string,
|
||||
filteredTags: string[],
|
||||
filteredTypes: LPE.ContentType[],
|
||||
) => {
|
||||
setBusy(true)
|
||||
const { data, errors } = await api.search({
|
||||
query,
|
||||
tags: filteredTags,
|
||||
type: filteredTypes,
|
||||
})
|
||||
setBusy(false)
|
||||
setResultNumber(data.posts.length || null)
|
||||
setPosts(data.posts as LPE.Search.ResultItemBase<LPE.Post.Document>[])
|
||||
setBlocks(data.blocks as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[])
|
||||
console.log(data)
|
||||
setQuery(query)
|
||||
setTags(filteredTags)
|
||||
setTypes(filteredTypes)
|
||||
}
|
||||
|
||||
let resultsNumber =
|
||||
types.includes(LPE.ContentTypes.Article) ||
|
||||
types.includes(LPE.ContentTypes.Podcast)
|
||||
? posts.length
|
||||
: blocks.length
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '80vh' }}>
|
||||
<SEO
|
||||
|
@ -64,12 +79,18 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
|
|||
<SearchBox
|
||||
tags={topics}
|
||||
onSearch={handleSearch}
|
||||
resultsNumber={resultNumber}
|
||||
busy={busy}
|
||||
resultsNumber={resultsNumber}
|
||||
busy={isLoading}
|
||||
onViewChange={setView}
|
||||
showModeSwitch={!isMobile}
|
||||
/>
|
||||
{view === 'list' && (
|
||||
<SearchResultsListView blocks={blocks} posts={posts} shows={shows} />
|
||||
<SearchResultsListView
|
||||
blocks={blocks}
|
||||
posts={posts}
|
||||
shows={shows}
|
||||
busy={isLoading}
|
||||
/>
|
||||
)}
|
||||
{view === 'explore' && (
|
||||
<SearchResultsExploreView blocks={blocks} posts={posts} shows={shows} />
|
||||
|
@ -78,9 +99,13 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
|
|||
)
|
||||
}
|
||||
|
||||
SearchPage.getLayout = (page: ReactNode) => (
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<DefaultLayout>{page}</DefaultLayout>
|
||||
</QueryParamProvider>
|
||||
)
|
||||
|
||||
export async function getStaticProps() {
|
||||
// const { data: articles = [] } = await unbodyApi.searchArticles()
|
||||
// const { data: blocks = [] } = await unbodyApi.searchBlocks()
|
||||
const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
|
||||
const { data: shows = [] } = await unbodyApi.getPodcastShows({
|
||||
populateEpisodes: true,
|
||||
|
@ -89,8 +114,6 @@ export async function getStaticProps() {
|
|||
|
||||
return {
|
||||
props: {
|
||||
// articles,
|
||||
// blocks: shuffle(blocks),
|
||||
topics,
|
||||
shows,
|
||||
},
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -4165,6 +4165,13 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next-query-params@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/next-query-params/-/next-query-params-4.2.3.tgz#f90f1a56de8ac6e6a6adb492a11f8854cced314d"
|
||||
integrity sha512-hGNCYRH8YyA5ItiBGSKrtMl21b2MAqfPkdI1mvwloNVqSU142IaGzqHN+OTovyeLIpQfonY01y7BAHb/UH4POg==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
next@13.3.0:
|
||||
version "13.3.0"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.3.0.tgz#40632d303d74fc8521faa0a5bf4a033a392749b1"
|
||||
|
@ -4963,6 +4970,11 @@ sentence-case@^3.0.4:
|
|||
tslib "^2.0.3"
|
||||
upper-case-first "^2.0.2"
|
||||
|
||||
serialize-query-params@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81"
|
||||
integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
@ -5530,6 +5542,13 @@ urlpattern-polyfill@^9.0.0:
|
|||
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz#bc7e386bb12fd7898b58d1509df21d3c29ab3460"
|
||||
integrity sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==
|
||||
|
||||
use-query-params@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d"
|
||||
integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==
|
||||
dependencies:
|
||||
serialize-query-params "^2.0.2"
|
||||
|
||||
use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
|
|
Loading…
Reference in New Issue