add article page search
This commit is contained in:
parent
a9db723aca
commit
c1f44b6f08
|
@ -1,15 +1,20 @@
|
|||
import { getBodyBlocks } from '@/utils/data.utils'
|
||||
import { RenderArticleBlock } from './Article.Block'
|
||||
import { ArticlePostData } from '@/types/data.types'
|
||||
import {
|
||||
GoogleDocEnhanced,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
|
||||
type Props = {
|
||||
data: ArticlePostData
|
||||
data: GoogleDocEnhanced
|
||||
}
|
||||
|
||||
const ArticleBlocks = ({ data }: Props) => {
|
||||
return data?.article.blocks.length ? (
|
||||
return data.blocks.length ? (
|
||||
<>
|
||||
{getBodyBlocks(data.article).map((block, idx) => (
|
||||
{getBodyBlocks(data).map((block, idx) => (
|
||||
<RenderArticleBlock key={'block-' + idx} block={block} />
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -5,18 +5,45 @@ import ArticleHeader from './Header/Article.Header'
|
|||
import ArticleFooter from './Footer/Article.Footer'
|
||||
import { MobileToc } from './Article.MobileToc'
|
||||
import ArticleBlocks from './Article.Blocks'
|
||||
import { useArticleContext } from '@/context/article.context'
|
||||
import { useSearchBarContext } from '@/context/searchbar.context'
|
||||
import { useEffect } from 'react'
|
||||
import { TextBlockEnhanced, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
|
||||
interface Props {
|
||||
data: ArticlePostData
|
||||
}
|
||||
|
||||
export default function ArticleBody({ data }: Props) {
|
||||
const { resultsNumber, setResultsHelperText } = useSearchBarContext()
|
||||
const { data: searchResultBlocks = [] } = useArticleContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (resultsNumber !== null) {
|
||||
setResultsHelperText(data.article.title)
|
||||
}
|
||||
}, [resultsNumber])
|
||||
|
||||
const ids = searchResultBlocks?.map((block) => block.doc._additional.id)
|
||||
|
||||
const blocks =
|
||||
resultsNumber !== null
|
||||
? data.article.blocks.filter((block) =>
|
||||
ids?.includes(block._additional.id),
|
||||
)
|
||||
: data.article.blocks
|
||||
|
||||
return (
|
||||
<ArticleContainer>
|
||||
<ArticleHeader {...data.article} />
|
||||
<MobileToc toc={data.article.toc} />
|
||||
{resultsNumber === null && <ArticleHeader {...data.article} />}
|
||||
{resultsNumber === null && <MobileToc toc={data.article.toc} />}
|
||||
<TextContainer>
|
||||
<ArticleBlocks data={data} />
|
||||
{/*@ts-ignore*/}
|
||||
<ArticleBlocks data={{ ...data.article, blocks }} />
|
||||
{resultsNumber === 0 && (
|
||||
<Typography variant="body1">No results found</Typography>
|
||||
)}
|
||||
</TextContainer>
|
||||
<ArticleFooter data={data} />
|
||||
</ArticleContainer>
|
||||
|
|
|
@ -7,7 +7,12 @@ const Main = ({ children }: PropsWithChildren) => {
|
|||
}
|
||||
|
||||
const Container = styled.main`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 1440px;
|
||||
margin-block: ${uiConfigs.postSectionMargin}px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
`
|
||||
|
||||
export default Main
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
|
||||
.globalSearchTrigger{
|
||||
text-decoration: underline;
|
||||
transition: font-size 150ms ease-in-out;
|
||||
}
|
||||
|
|
|
@ -23,13 +23,19 @@ import {
|
|||
extractTopicsFromQuery,
|
||||
} from '@/utils/search.utils'
|
||||
import Link from 'next/link'
|
||||
import { useSearchBarContext } from '@/context/searchbar.context'
|
||||
|
||||
export type SearchbarProps = {
|
||||
searchScope?: ESearchScope
|
||||
className?: string
|
||||
onSearch?: (query: string, filterTags: string[]) => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
export default function Searchbar(props: SearchbarProps) {
|
||||
const { onSearch, onReset } = props
|
||||
const { resultsNumber, resultsHelperText } = useSearchBarContext()
|
||||
|
||||
const [searchScope, setSearchScope] = useState<ESearchScope>(
|
||||
props.searchScope || ESearchScope.GLOBAL,
|
||||
)
|
||||
|
@ -46,10 +52,20 @@ export default function Searchbar(props: SearchbarProps) {
|
|||
const isValidSearchInput = (_filterTags: string[] = []) =>
|
||||
(query && query.length > 0) || _filterTags.length > 0
|
||||
|
||||
const isArticlePage = router.pathname === '/article/[slug]'
|
||||
|
||||
const performSearch = async (
|
||||
q: string = query,
|
||||
_filterTags: string[] = filterTags,
|
||||
) => {
|
||||
//if it is article page, just call onSearch
|
||||
if (isArticlePage) {
|
||||
if (onSearch) {
|
||||
onSearch(q, _filterTags)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await router.push(
|
||||
{
|
||||
pathname: '/search',
|
||||
|
@ -74,7 +90,15 @@ export default function Searchbar(props: SearchbarProps) {
|
|||
}, [router.query.query, router.query.topics])
|
||||
|
||||
const performClear = useCallback(() => {
|
||||
performSearch('', [])
|
||||
if (!isArticlePage) {
|
||||
performSearch('', [])
|
||||
return
|
||||
}
|
||||
|
||||
setQuery('')
|
||||
setFilterTags([])
|
||||
setActive(false)
|
||||
onReset && onReset()
|
||||
}, [setQuery, setFilterTags])
|
||||
|
||||
const handleTagClick = (tag: string) => {
|
||||
|
@ -112,6 +136,16 @@ export default function Searchbar(props: SearchbarProps) {
|
|||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
}}
|
||||
style={{
|
||||
transition: 'height 150ms ease-in-out',
|
||||
height: active ? '56px' : 'auto',
|
||||
}}
|
||||
inputProps={{
|
||||
style: {
|
||||
fontSize: active ? '28px' : '14px',
|
||||
transition: 'font-size 150ms ease-in-out',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{searchScope === ESearchScope.ARTICLE && (
|
||||
<GlobalSearchTrigger
|
||||
|
@ -141,6 +175,17 @@ export default function Searchbar(props: SearchbarProps) {
|
|||
selectedTags={filterTags}
|
||||
/>
|
||||
</TagsWrapper>
|
||||
{resultsNumber !== null && (
|
||||
<ResultsStatus>
|
||||
<Typography variant={'subtitle2'}>{resultsNumber} matches</Typography>
|
||||
{resultsHelperText && (
|
||||
<Typography variant={'subtitle2'}>.</Typography>
|
||||
)}
|
||||
{resultsHelperText && (
|
||||
<Typography variant={'subtitle2'}>{resultsHelperText}</Typography>
|
||||
)}
|
||||
</ResultsStatus>
|
||||
)}
|
||||
<Collapsed
|
||||
className={isCollapsed ? 'enabled' : ''}
|
||||
onClick={() => setActive(true)}
|
||||
|
@ -153,15 +198,26 @@ export default function Searchbar(props: SearchbarProps) {
|
|||
}
|
||||
|
||||
const TagsWrapper = styled.div`
|
||||
transition: height 250ms ease-in-out;
|
||||
transition: height 150ms ease-in-out;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
|
||||
&.active {
|
||||
height: 45px;
|
||||
}
|
||||
`
|
||||
|
||||
const ResultsStatus = styled.div`
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
grid-column-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
> :nth-child(2) {
|
||||
font-size: 18px;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
`
|
||||
|
||||
const Collapsed = styled.div`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
@ -174,9 +230,9 @@ const Collapsed = styled.div`
|
|||
top: -100%;
|
||||
left: 0;
|
||||
|
||||
font-size: 14px;
|
||||
font-size: 28px;
|
||||
|
||||
transition: top 250ms ease-in-out;
|
||||
transition: top 150ms ease-in-out;
|
||||
|
||||
&.enabled {
|
||||
top: 0;
|
||||
|
@ -206,7 +262,8 @@ const GlobalSearchTrigger = styled(Link)`
|
|||
left: 256px;
|
||||
top: 7px;
|
||||
|
||||
transition: opacity 250ms ease-in-out;
|
||||
transition: opacity 50ms;
|
||||
transition-delay: 50ms;
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
|
|
|
@ -4,6 +4,8 @@ import { useSticky } from '@/utils/ui.utils'
|
|||
import { Typography } from '@acid-info/lsd-react'
|
||||
import styled from '@emotion/styled'
|
||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||
import { useArticleContext } from '@/context/article.context'
|
||||
import { useSearchBarContext } from '@/context/searchbar.context'
|
||||
|
||||
export type TableOfContentsProps = Pick<UnbodyGoogleDoc, 'toc'>
|
||||
|
||||
|
@ -15,6 +17,7 @@ export default function TableOfContents({ contents, ...props }: Props) {
|
|||
const articleContainer = useArticleContainerContext()
|
||||
const { tocIndex, setTocIndex } = articleContainer
|
||||
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin
|
||||
const { resultsNumber } = useSearchBarContext()
|
||||
|
||||
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
||||
|
||||
|
@ -28,7 +31,6 @@ export default function TableOfContents({ contents, ...props }: Props) {
|
|||
top: Number(position?.top) + window.scrollY - 100,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
|
||||
setTocIndex(index)
|
||||
}
|
||||
|
||||
|
@ -38,7 +40,9 @@ export default function TableOfContents({ contents, ...props }: Props) {
|
|||
height={height}
|
||||
ref={stickyRef}
|
||||
{...props}
|
||||
className={sticky ? 'sticky' : ''}
|
||||
className={`${resultsNumber !== null ? 'hidden' : ''} ${
|
||||
sticky ? 'sticky' : ''
|
||||
}`}
|
||||
>
|
||||
<Title variant="body3">Contents</Title>
|
||||
<Contents height={height}>
|
||||
|
@ -71,6 +75,11 @@ const Container = styled.aside<{ dy: number; height: number }>`
|
|||
margin-left: 16px;
|
||||
padding-bottom: 72px;
|
||||
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// temporary breakpoint
|
||||
@media (max-width: 1024px) {
|
||||
display: none;
|
||||
|
|
|
@ -4,6 +4,8 @@ import { useState } from 'react'
|
|||
import { ArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||
import ArticleBody from '@/components/Article/Article.Body'
|
||||
import { ArticlePostData } from '@/types/data.types'
|
||||
import { useArticleContext } from '@/context/article.context'
|
||||
import { Grid, GridItem } from '@/components/Grid/Grid'
|
||||
|
||||
interface Props {
|
||||
data: ArticlePostData
|
||||
|
@ -14,27 +16,29 @@ const ArticleContainer = (props: Props) => {
|
|||
const [tocIndex, setTocIndex] = useState(0)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ArticleContainerContext.Provider
|
||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||
<ArticleContainerContext.Provider
|
||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
||||
>
|
||||
<Grid
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableOfContents contents={data.article.toc ?? []} />
|
||||
<ArticleBody data={data} />
|
||||
<Right />
|
||||
</ArticleContainerContext.Provider>
|
||||
</Container>
|
||||
<Gap className={'w-1'} />
|
||||
<GridItem className={'w-2'}>
|
||||
<TableOfContents contents={data.article.toc ?? []} />
|
||||
</GridItem>
|
||||
<Gap className={'w-1'} />
|
||||
<GridItem className={'w-8'}>
|
||||
<ArticleBody data={data} />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</ArticleContainerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const Right = styled.aside`
|
||||
width: 162px;
|
||||
// temporary breakpoint
|
||||
@media (max-width: 1024px) {
|
||||
const Gap = styled(GridItem)`
|
||||
@media (max-width: 550px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { useArticleSearch, useSearchGeneric } from '@/hooks/useSearch'
|
||||
import { UnbodyImageBlock, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
||||
import { PostTypes, SearchHook } from '@/types/data.types'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { useSearchBarContext } from './searchbar.context'
|
||||
|
||||
type ArticleContext = SearchHook<UnbodyTextBlock | UnbodyImageBlock> & {
|
||||
onSearch: (query: string, filters: string[], title: string) => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
const ArticleContext = createContext<ArticleContext>({
|
||||
onSearch: () => {},
|
||||
data: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
search: () => Promise.resolve([]),
|
||||
reset: () => {},
|
||||
onReset: () => {},
|
||||
})
|
||||
|
||||
export const ArticleProvider = ({ children }: any) => {
|
||||
const {
|
||||
query: { slug },
|
||||
} = useRouter()
|
||||
|
||||
const blocks = useArticleSearch([])
|
||||
const { setResultsNumber, setResultsHelperText } = useSearchBarContext()
|
||||
|
||||
const onSearch = async (query: string = '', filters: string[] = []) => {
|
||||
if (query.trim().length == 0) return
|
||||
setResultsHelperText('Searching...')
|
||||
const res = await blocks.search(query, filters, slug)
|
||||
setResultsHelperText(null)
|
||||
setResultsNumber(res.length)
|
||||
}
|
||||
|
||||
const onReset = async () => {
|
||||
setResultsNumber(null)
|
||||
setResultsHelperText(null)
|
||||
blocks.reset([])
|
||||
}
|
||||
|
||||
return (
|
||||
<ArticleContext.Provider
|
||||
value={{
|
||||
...blocks,
|
||||
onSearch,
|
||||
onReset,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ArticleContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Export context
|
||||
export default ArticleContext
|
||||
|
||||
export const useArticleContext = () => useContext(ArticleContext)
|
|
@ -0,0 +1,43 @@
|
|||
// context for searchbar
|
||||
import { SearchbarProps } from '@/components/Searchbar/Searchbar'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
type SearchBarContext = SearchbarProps & {
|
||||
resultsNumber: number | null
|
||||
setResultsNumber: (resultsNumber: number | null) => void
|
||||
resultsHelperText: string | null
|
||||
setResultsHelperText: (resultsHelperText: string | null) => void
|
||||
}
|
||||
|
||||
const SearchBarContext = createContext<SearchBarContext>({
|
||||
resultsNumber: null,
|
||||
setResultsNumber: () => {},
|
||||
resultsHelperText: null,
|
||||
setResultsHelperText: () => {},
|
||||
})
|
||||
|
||||
export const SearchBarProvider = ({ children }: any) => {
|
||||
const [resultsNumber, setResultsNumber] = useState<number | null>(null)
|
||||
const [resultsHelperText, setResultsHelperText] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
return (
|
||||
<SearchBarContext.Provider
|
||||
value={{
|
||||
resultsNumber,
|
||||
setResultsNumber,
|
||||
resultsHelperText,
|
||||
setResultsHelperText,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SearchBarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Export context
|
||||
export default SearchBarContext
|
||||
|
||||
export const useSearchBarContext = () =>
|
||||
useContext<SearchBarContext>(SearchBarContext)
|
|
@ -38,3 +38,32 @@ export const useSearchGeneric = <T>(
|
|||
}
|
||||
return { data, loading, error, search, reset }
|
||||
}
|
||||
|
||||
export const useArticleSearch = (
|
||||
initialData: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[],
|
||||
): SearchHook<UnbodyImageBlock | UnbodyTextBlock> => {
|
||||
const [data, setData] =
|
||||
useState<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>(
|
||||
initialData,
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const search = async (query: string, tags: string[], slug: string) => {
|
||||
if (loading) return Promise.resolve([])
|
||||
setLoading(true)
|
||||
const result = await searchApi.searchArticle(query, tags, slug)
|
||||
setData(result.data)
|
||||
setLoading(false)
|
||||
return result.data
|
||||
}
|
||||
|
||||
const reset = (
|
||||
_initialData: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[],
|
||||
) => {
|
||||
setData(_initialData)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
}
|
||||
return { data, loading, error, search, reset }
|
||||
}
|
||||
|
|
|
@ -7,17 +7,27 @@ import { ESearchScope } from '@/types/ui.types'
|
|||
import styles from './Article.layout.module.css'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { Main } from '@/components/Main'
|
||||
import { useArticleContext } from '@/context/article.context'
|
||||
|
||||
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||
type Props = PropsWithChildren<{
|
||||
// onSearch: (query: string, filters: string[]) => void
|
||||
}>
|
||||
export default function ArticleLayout({ children }: Props) {
|
||||
const isDarkState = useIsDarkState()
|
||||
const { onSearch, onReset } = useArticleContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||
<NavbarFiller />
|
||||
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||
<Searchbar
|
||||
searchScope={ESearchScope.ARTICLE}
|
||||
onSearch={onSearch}
|
||||
onReset={onReset}
|
||||
/>
|
||||
</header>
|
||||
<Main>{props.children}</Main>
|
||||
<Main>{children}</Main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import { uiConfigs } from '@/configs/ui.configs'
|
|||
import { DefaultLayout } from '@/layouts/DefaultLayout'
|
||||
import { ReactNode } from 'react'
|
||||
import { NextComponentType, NextPageContext } from 'next'
|
||||
import { SearchBarProvider } from '@/context/searchbar.context'
|
||||
|
||||
type NextLayoutComponentType<P = {}> = NextComponentType<
|
||||
NextPageContext,
|
||||
|
@ -52,7 +53,9 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
|
|||
}
|
||||
`}
|
||||
/>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
<SearchBarProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SearchBarProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import api from '@/services/unbody.service'
|
||||
import { PostTypes } from '@/types/data.types'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>,
|
||||
) {
|
||||
const {
|
||||
query: { q = '', tags: tagsString = '', slug },
|
||||
} = req
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: 'Invalid request' })
|
||||
}
|
||||
|
||||
const tags =
|
||||
typeof tagsString === 'string'
|
||||
? tagsString
|
||||
.split(',')
|
||||
.map((tag: string) => tag.trim())
|
||||
.filter((t) => t.length > 0)
|
||||
: undefined
|
||||
|
||||
const response = await api.searchBlockInArticle(
|
||||
q as string,
|
||||
tags,
|
||||
true,
|
||||
slug as string,
|
||||
)
|
||||
res.status(200).json(response)
|
||||
}
|
|
@ -5,6 +5,7 @@ import ArticleContainer from '@/containers/ArticleContainer'
|
|||
import api from '@/services/unbody.service'
|
||||
import { ArticlePostData } from '@/types/data.types'
|
||||
import { SEO } from '@/components/SEO'
|
||||
import { ArticleProvider } from '@/context/article.context'
|
||||
|
||||
type ArticleProps = {
|
||||
data: ArticlePostData
|
||||
|
@ -66,7 +67,11 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
|||
}
|
||||
|
||||
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
||||
return <ArticleLayout>{page}</ArticleLayout>
|
||||
return (
|
||||
<ArticleProvider>
|
||||
<ArticleLayout>{page}</ArticleLayout>
|
||||
</ArticleProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticlePage
|
||||
|
|
|
@ -23,6 +23,9 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||
alt
|
||||
order
|
||||
__typename
|
||||
_additional{
|
||||
id
|
||||
}
|
||||
}
|
||||
... on TextBlock {
|
||||
footnotes
|
||||
|
@ -32,6 +35,9 @@ export const getArticlePostQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||
tagName
|
||||
__typename
|
||||
classNames
|
||||
_additional{
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
_additional{
|
||||
|
|
|
@ -27,6 +27,7 @@ export const getSearchBlocksQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||
}
|
||||
_additional{
|
||||
certainty
|
||||
id
|
||||
}
|
||||
`),
|
||||
GetImageBlockQuery(args)(`
|
||||
|
@ -46,6 +47,7 @@ export const getSearchBlocksQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
|||
}
|
||||
_additional{
|
||||
certainty
|
||||
id
|
||||
}
|
||||
`),
|
||||
].join(' '),
|
||||
|
|
|
@ -2,8 +2,22 @@ import { PostTypes } from '@/types/data.types'
|
|||
|
||||
class SearchService {
|
||||
constructor() {}
|
||||
|
||||
serach = (query: string, tags: string[], postType: PostTypes) => {
|
||||
return fetch(`/api/search/${postType}?q=${query}&tags=${tags.join(',')}`)
|
||||
return fetch(
|
||||
`/api/search/general/${postType}?q=${query}&tags=${tags.join(',')}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return { data: null, errors: JSON.stringify(e) }
|
||||
})
|
||||
}
|
||||
|
||||
searchArticle = (query: string, tags: string[], slug: string) => {
|
||||
return fetch(
|
||||
`/api/search/article/${slug}?q=${query}&tags=${tags.join(',')}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
|
|
|
@ -40,6 +40,7 @@ type HomepagePost = Pick<
|
|||
| 'blocks'
|
||||
| 'mentions'
|
||||
| 'slug'
|
||||
| '_additional'
|
||||
>
|
||||
|
||||
type HomePageData = {
|
||||
|
@ -66,13 +67,14 @@ const OperandFactory = (
|
|||
})
|
||||
|
||||
export const Operands: Record<string, (...a: any) => WhereOperandsInpObj> = {
|
||||
WHERE_PUBLISHED: (subPath: string[] = ['pathString']) =>
|
||||
OperandFactory(
|
||||
WHERE_PUBLISHED: (subPath: string[] = ['pathString']) => {
|
||||
return OperandFactory(
|
||||
UnbodyGraphQl.Filters.WhereOperatorEnum.Like,
|
||||
[...subPath],
|
||||
'*/published/*',
|
||||
'valueString',
|
||||
),
|
||||
)
|
||||
},
|
||||
WHERE_ID_IS: (id) =>
|
||||
OperandFactory(
|
||||
UnbodyGraphQl.Filters.WhereOperatorEnum.Equal,
|
||||
|
@ -101,6 +103,13 @@ export const Operands: Record<string, (...a: any) => WhereOperandsInpObj> = {
|
|||
author,
|
||||
'valueText',
|
||||
),
|
||||
WHERE_IS_IN_SLUG: (slug: string) =>
|
||||
OperandFactory(
|
||||
UnbodyGraphQl.Filters.WhereOperatorEnum.Equal,
|
||||
['document', UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc, 'slug'],
|
||||
slug,
|
||||
'valueString',
|
||||
),
|
||||
}
|
||||
|
||||
const mapSearchResultItem = <T extends UnbodyDocTypes>(
|
||||
|
@ -160,7 +169,7 @@ class UnbodyService extends UnbodyClient {
|
|||
if (errors || !data) {
|
||||
console.log(errors)
|
||||
return {
|
||||
data: null as any,
|
||||
data: data as any,
|
||||
errors: JSON.stringify(errors),
|
||||
}
|
||||
}
|
||||
|
@ -280,29 +289,36 @@ class UnbodyService extends UnbodyClient {
|
|||
.catch((e) => this.handleResponse([], e))
|
||||
}
|
||||
|
||||
serachBlocks = async (
|
||||
searchBlockInArticle = async (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
published: boolean = true,
|
||||
articleSlug: string,
|
||||
): Promise<
|
||||
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
||||
ApiResponse<SearchResultItem<UnbodyImageBlock | TextBlockEnhanced>[]>
|
||||
> => {
|
||||
const query = getSearchBlocksQuery({
|
||||
...(q.trim().length > 0
|
||||
? {
|
||||
nearText: {
|
||||
concepts: [q, ...tags],
|
||||
certainty: 0.85,
|
||||
certainty: 0.8,
|
||||
},
|
||||
where: {
|
||||
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
|
||||
operands: [
|
||||
...(published
|
||||
? [
|
||||
Operands.WHERE_PUBLISHED([
|
||||
'document',
|
||||
UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc,
|
||||
'pathString',
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
Operands.WHERE_IS_IN_SLUG(articleSlug),
|
||||
],
|
||||
},
|
||||
...(published
|
||||
? {
|
||||
where: Operands.WHERE_PUBLISHED([
|
||||
'document',
|
||||
'GoogleDoc',
|
||||
'pathString',
|
||||
]),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
@ -311,7 +327,65 @@ class UnbodyService extends UnbodyClient {
|
|||
.then(({ data }) => {
|
||||
if (!data || !(data.Get.ImageBlock || data.Get.TextBlock))
|
||||
return this.handleResponse([], 'No data')
|
||||
const blocks = [...data.Get.ImageBlock, ...data.Get.TextBlock]
|
||||
|
||||
const blocks = [
|
||||
...(data.Get.ImageBlock || []),
|
||||
...(data.Get.TextBlock || []),
|
||||
]
|
||||
|
||||
return this.handleResponse(
|
||||
blocks
|
||||
.map((block) => {
|
||||
if (
|
||||
block.__typename ===
|
||||
UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock
|
||||
) {
|
||||
return mapSearchResultItem(q, tags, enhanceTextBlock(block))
|
||||
}
|
||||
return mapSearchResultItem(q, tags, block)
|
||||
})
|
||||
.sort((a, b) => b.score - a.score),
|
||||
)
|
||||
})
|
||||
.catch((e) => this.handleResponse([], e))
|
||||
}
|
||||
|
||||
serachBlocks = async (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
published: boolean = true,
|
||||
): Promise<
|
||||
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
||||
> => {
|
||||
let whereFilters: any = []
|
||||
|
||||
const query = getSearchBlocksQuery({
|
||||
...(q.trim().length > 0
|
||||
? {
|
||||
nearText: {
|
||||
concepts: [q, ...tags],
|
||||
certainty: 0.85,
|
||||
},
|
||||
where: Operands.WHERE_PUBLISHED([
|
||||
'document',
|
||||
UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc,
|
||||
'pathString',
|
||||
]),
|
||||
limit: 20,
|
||||
}
|
||||
: { limit: 20 }),
|
||||
})
|
||||
|
||||
return this.request<UnbodyGraphQlResponseBlocks>(query)
|
||||
.then(({ data }) => {
|
||||
if (!data || !(data.Get.ImageBlock || data.Get.TextBlock))
|
||||
return this.handleResponse([], 'No data')
|
||||
|
||||
const blocks = [
|
||||
...(data.Get.ImageBlock || []),
|
||||
...(data.Get.TextBlock || []),
|
||||
]
|
||||
|
||||
return this.handleResponse(
|
||||
blocks
|
||||
.map((block) => mapSearchResultItem(q, tags, block))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
GoogleDocEnhanced,
|
||||
TextBlockEnhanced,
|
||||
UnbodyGoogleDoc,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
|
@ -14,6 +15,7 @@ export enum PostTypes {
|
|||
export interface ArticlePostData {
|
||||
article: GoogleDocEnhanced & {
|
||||
toc: Array<UnbodyGraphQl.Fragments.TocItem>
|
||||
blocks: Array<UnbodyImageBlock | TextBlockEnhanced>
|
||||
}
|
||||
relatedArticles: Array<GoogleDocEnhanced>
|
||||
articlesFromSameAuthors: Array<GoogleDocEnhanced>
|
||||
|
@ -58,6 +60,10 @@ export type SearchHook<T> = {
|
|||
data: SearchResultItem<T>[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
search: (query: string, tags: string[]) => Promise<SearchResultItem<T>[]>
|
||||
search: (
|
||||
query: string,
|
||||
tags: string[],
|
||||
...args: any
|
||||
) => Promise<SearchResultItem<T>[]>
|
||||
reset: (initialData: SearchResultItem<T>[]) => void
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
GoogleDocEnhanced,
|
||||
TextBlockEnhanced,
|
||||
UnbodyGoogleDoc,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
|
@ -13,12 +14,19 @@ function hasClassName(inputString: string, className: string) {
|
|||
return regex.test(inputString)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
blocks: Array<UnbodyImageBlock | TextBlockEnhanced>
|
||||
summary: string
|
||||
tags: string[]
|
||||
mentions: any[]
|
||||
}
|
||||
|
||||
export const getBodyBlocks = ({
|
||||
blocks,
|
||||
summary,
|
||||
tags = [],
|
||||
mentions = [],
|
||||
}: GoogleDocEnhanced) => {
|
||||
}: Props) => {
|
||||
return (blocks || []).filter((b) => {
|
||||
const classNames = b.classNames || []
|
||||
|
||||
|
|
Loading…
Reference in New Issue