mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 14:48:08 +00:00
compete search
This commit is contained in:
parent
2aa98625cf
commit
3e5b8c6ec8
@ -11,7 +11,15 @@ import { ESearchScope, ESearchStatus } from '@/types/ui.types'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import FilterTags from '@/components/FilterTags/FilterTags'
|
||||
import styled from '@emotion/styled'
|
||||
import { useSearchContext } from '@/context/SearchContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import {
|
||||
addQueryToQuery,
|
||||
addTopicsToQuery,
|
||||
createMinimizedSearchText,
|
||||
extractQueryFromQuery,
|
||||
extractTopicsFromQuery,
|
||||
} from '@/utils/search.utils'
|
||||
|
||||
export type SearchbarProps = {
|
||||
searchScope?: ESearchScope
|
||||
@ -21,25 +29,42 @@ export type SearchbarProps = {
|
||||
export default function Searchbar(props: SearchbarProps) {
|
||||
const { searchScope = ESearchScope.GLOBAL } = props
|
||||
const [active, setActive] = useState(false)
|
||||
const { exec } = useSearchContext()
|
||||
const router = useRouter()
|
||||
|
||||
// const [loading, setLoading] = useState(false)
|
||||
// const [error, setError] = useState(false)
|
||||
const [query, setQuery] = useState<string>('')
|
||||
const [filterTags, setFilterTags] = useState<string[]>([])
|
||||
const [query, setQuery] = useState<string>(
|
||||
extractQueryFromQuery(router.query),
|
||||
)
|
||||
const [filterTags, setFilterTags] = useState<string[]>(
|
||||
extractTopicsFromQuery(router.query),
|
||||
)
|
||||
|
||||
const isValidSearchInput = () =>
|
||||
(query && query.length > 0) || filterTags.length > 0
|
||||
const isValidSearchInput = (_filterTags: string[] = []) =>
|
||||
(query && query.length > 0) || _filterTags.length > 0
|
||||
|
||||
const performSearch = async () => {
|
||||
if (!isValidSearchInput()) return
|
||||
exec(query, filterTags)
|
||||
const performSearch = async (
|
||||
q: string = query,
|
||||
_filterTags: string[] = filterTags,
|
||||
) => {
|
||||
await router.push(
|
||||
{
|
||||
pathname: '/search',
|
||||
query: {
|
||||
...addQueryToQuery(q),
|
||||
...addTopicsToQuery(_filterTags),
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(extractQueryFromQuery(router.query))
|
||||
setFilterTags(extractTopicsFromQuery(router.query))
|
||||
}, [router.query.query, router.query.topics])
|
||||
|
||||
const performClear = useCallback(() => {
|
||||
// TODO: clear input.value seems to be not working. When set to undefined, the input value is still there.
|
||||
setQuery('')
|
||||
setFilterTags([])
|
||||
performSearch('', [])
|
||||
}, [setQuery, setFilterTags])
|
||||
|
||||
const handleTagClick = (tag: string) => {
|
||||
@ -49,7 +74,7 @@ export default function Searchbar(props: SearchbarProps) {
|
||||
} else {
|
||||
newSelectedTags.push(tag)
|
||||
}
|
||||
setFilterTags(newSelectedTags)
|
||||
performSearch(query, newSelectedTags)
|
||||
}
|
||||
|
||||
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@ -58,18 +83,6 @@ export default function Searchbar(props: SearchbarProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const constructCollapseText = () => {
|
||||
let txt = ''
|
||||
if (query !== undefined && query.length > 0) {
|
||||
txt += `<span>${query}</span>`
|
||||
}
|
||||
if (filterTags.length > 0) {
|
||||
if (txt.length > 0) txt += '<b> . </b>'
|
||||
txt += `${filterTags.map((t) => `<small>[${t}]</small>`).join(' ')}`
|
||||
}
|
||||
return txt
|
||||
}
|
||||
|
||||
const isCollapsed = isValidSearchInput() && !active
|
||||
|
||||
const placeholder =
|
||||
@ -111,7 +124,9 @@ export default function Searchbar(props: SearchbarProps) {
|
||||
<Collapsed
|
||||
className={isCollapsed ? 'enabled' : ''}
|
||||
onClick={() => setActive(true)}
|
||||
dangerouslySetInnerHTML={{ __html: constructCollapseText() }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: createMinimizedSearchText(query, filterTags),
|
||||
}}
|
||||
/>
|
||||
</SearchbarContainer>
|
||||
)
|
||||
@ -146,9 +161,11 @@ const Collapsed = styled.div`
|
||||
&.enabled {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { useArticlesSearch } from '@/hooks/useSearch'
|
||||
|
||||
type SearchContainerProps = {}
|
||||
|
||||
export const SearchContainer = (props: SearchContainerProps) => {
|
||||
return <div>Search result container...</div>
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import { useArticlesSearch } from '@/hooks/useSearch'
|
||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||
import { SearchResultItem } from '@/types/data.types'
|
||||
import { ESearchStatus } from '@/types/ui.types'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
type SearchContextType = {
|
||||
status: ESearchStatus
|
||||
exec: (
|
||||
q?: string,
|
||||
tags?: string[],
|
||||
) => Promise<SearchResultItem<UnbodyGoogleDoc>[] | null>
|
||||
}
|
||||
|
||||
const SearchContext = createContext<SearchContextType>({
|
||||
status: ESearchStatus.NOT_ACTIVE,
|
||||
exec: () => {},
|
||||
} as SearchContextType)
|
||||
|
||||
export const SearchContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [status, setStatus] = useState<ESearchStatus>(ESearchStatus.NOT_ACTIVE)
|
||||
const { data, loading, search } = useArticlesSearch()
|
||||
|
||||
const exec = async (q: string = '', tags: string[] = []) => {
|
||||
setStatus(ESearchStatus.SEARCHING)
|
||||
await search(q, tags)
|
||||
setStatus(ESearchStatus.IDLE)
|
||||
return data
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchContext.Provider
|
||||
value={{
|
||||
status,
|
||||
exec,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSearchContext = () => useContext(SearchContext)
|
@ -1,21 +1,62 @@
|
||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
||||
import {
|
||||
UnbodyGoogleDoc,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
import searchApi from '@/services/search.service'
|
||||
import { SearchResultItem } from '@/types/data.types'
|
||||
import {
|
||||
SearchHook,
|
||||
SearchHookDataPayload,
|
||||
SearchResultItem,
|
||||
SearchResults,
|
||||
} from '@/types/data.types'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useArticlesSearch = () => {
|
||||
const [data, setData] = useState<SearchResultItem<UnbodyGoogleDoc>[] | null>(
|
||||
null,
|
||||
)
|
||||
export const useSearchGeneric = <T>(
|
||||
initialData: SearchResultItem<T>[],
|
||||
): SearchHook<T> => {
|
||||
const [data, setData] = useState<SearchResultItem<T>[]>(initialData)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const search = async (query: string, tags: string[]) => {
|
||||
if (loading) return null
|
||||
setLoading(true)
|
||||
const result = await searchApi.searchArticles(query, tags)
|
||||
setData(result)
|
||||
setData(result.data)
|
||||
setLoading(false)
|
||||
return result.data
|
||||
}
|
||||
|
||||
return { data, loading, error, search }
|
||||
const reset = (_initialData: SearchResultItem<T>[]) => {
|
||||
setData(_initialData)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
}
|
||||
return { data, loading, error, search, reset }
|
||||
}
|
||||
|
||||
export const useSearch = (
|
||||
initialData: SearchHookDataPayload,
|
||||
): SearchResults => {
|
||||
const articles = useSearchGeneric<UnbodyGoogleDoc>(
|
||||
initialData?.articles ?? null,
|
||||
)
|
||||
const blocks = useSearchGeneric<UnbodyImageBlock | UnbodyTextBlock>(
|
||||
initialData?.blocks ?? null,
|
||||
)
|
||||
|
||||
const search = async (query: string, tags: string[]) => {
|
||||
const [articlesResult, blocksResult] = await Promise.all([
|
||||
articles.loading ? () => {} : articles.search(query, tags),
|
||||
blocks.loading ? () => {} : blocks.search(query, tags),
|
||||
])
|
||||
}
|
||||
|
||||
const reset = (initialData: SearchHookDataPayload) => {
|
||||
articles.reset(initialData?.articles)
|
||||
blocks.reset(initialData?.blocks)
|
||||
}
|
||||
|
||||
return { search, reset, blocks, articles }
|
||||
}
|
||||
|
@ -70,8 +70,17 @@ export namespace UnbodyGraphQl {
|
||||
}
|
||||
}
|
||||
|
||||
export enum UnbodyDocumentTypeNames {
|
||||
GoogleDoc = 'GoogleDoc',
|
||||
GoogleCalendarEvent = 'GoogleCalendarEvent',
|
||||
TextBlock = 'TextBlock',
|
||||
ImageBlock = 'ImageBlock',
|
||||
AudioFile = 'AudioFile',
|
||||
}
|
||||
|
||||
export interface BaseObject {
|
||||
_additional: Additional.AdditionalProps
|
||||
__typename: UnbodyDocumentTypeNames
|
||||
}
|
||||
|
||||
export interface BaseObjectWithRef<T> extends BaseObject {
|
||||
@ -80,6 +89,7 @@ export namespace UnbodyGraphQl {
|
||||
|
||||
export interface Beacon {
|
||||
beacon: string
|
||||
__typename: 'Beacon'
|
||||
}
|
||||
|
||||
export namespace Fragments {
|
||||
@ -89,7 +99,8 @@ export namespace UnbodyGraphQl {
|
||||
}
|
||||
|
||||
export interface ImageBlock
|
||||
extends BaseObjectWithRef<GoogleDoc | GoogleCalendarEvent | Beacon> {
|
||||
extends BaseObjectWithRef<GoogleDoc | GoogleCalendarEvent> {
|
||||
__typename: UnbodyDocumentTypeNames.ImageBlock
|
||||
alt: string
|
||||
createdAt: string
|
||||
ext: string
|
||||
@ -107,7 +118,8 @@ export namespace UnbodyGraphQl {
|
||||
width: number
|
||||
}
|
||||
|
||||
export interface TextBlock extends BaseObjectWithRef<GoogleDoc | Beacon> {
|
||||
export interface TextBlock extends BaseObjectWithRef<GoogleDoc> {
|
||||
__typename: UnbodyDocumentTypeNames.TextBlock
|
||||
footnotes: string | Array<Fragments.FootnoteItem>
|
||||
html: string
|
||||
order: number
|
||||
@ -118,7 +130,7 @@ export namespace UnbodyGraphQl {
|
||||
}
|
||||
|
||||
export interface AudioFile
|
||||
extends BaseObjectWithRef<GoogleDoc | GoogleCalendarEvent | Beacon> {
|
||||
extends BaseObjectWithRef<GoogleDoc | GoogleCalendarEvent> {
|
||||
duration: number
|
||||
ext: string
|
||||
mimeType: string
|
||||
@ -128,9 +140,11 @@ export namespace UnbodyGraphQl {
|
||||
size: number
|
||||
sourceId: string
|
||||
url: string
|
||||
__typename: UnbodyDocumentTypeNames.AudioFile
|
||||
}
|
||||
|
||||
export interface GoogleDoc extends BaseObject {
|
||||
__typename: UnbodyDocumentTypeNames.GoogleDoc
|
||||
blocks: Array<ImageBlock | TextBlock>
|
||||
createdAt: string
|
||||
html: string
|
||||
@ -151,6 +165,7 @@ export namespace UnbodyGraphQl {
|
||||
}
|
||||
|
||||
export interface GoogleCalendarEvent extends BaseObject {
|
||||
__typename: UnbodyDocumentTypeNames.GoogleCalendarEvent
|
||||
createdAt: string
|
||||
creatorDisplayName: string
|
||||
creatorEmail: string
|
||||
|
@ -30,3 +30,8 @@ export type UnbodyGraphQlResponseTextBlock = UnbodyGraphQlResponse<{
|
||||
export type UnbodyGraphQlResponseImageBlock = UnbodyGraphQlResponse<{
|
||||
ImageBlock: UnbodyImageBlock[]
|
||||
}>
|
||||
|
||||
export type UnbodyGraphQlResponseBlocks = UnbodyGraphQlResponse<{
|
||||
ImageBlock: UnbodyImageBlock[]
|
||||
TextBlock: UnbodyTextBlock[]
|
||||
}>
|
||||
|
@ -7,7 +7,6 @@ import { uiConfigs } from '@/configs/ui.configs'
|
||||
import { DefaultLayout } from '@/layouts/DefaultLayout'
|
||||
import { ReactNode } from 'react'
|
||||
import { NextComponentType, NextPageContext } from 'next'
|
||||
import { SearchContextProvider } from '@/context/SearchContext'
|
||||
|
||||
type NextLayoutComponentType<P = {}> = NextComponentType<
|
||||
NextPageContext,
|
||||
@ -51,9 +50,7 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<SearchContextProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SearchContextProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { PostDataProps } from '@/components/Post/Post'
|
||||
import PostsDemo from '@/components/Post/PostsDemo'
|
||||
import { SearchContainer } from '@/containers/SearchContainer'
|
||||
import { useSearchContext } from '@/context/SearchContext'
|
||||
import { UnbodyGoogleDoc, UnbodyImageBlock } from '@/lib/unbody/unbody.types'
|
||||
import api from '@/services/unbody.service'
|
||||
import { ESearchStatus } from '@/types/ui.types'
|
||||
@ -13,15 +11,9 @@ type Props = {
|
||||
}
|
||||
|
||||
export default function Home({ posts }: Props) {
|
||||
const { status } = useSearchContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === ESearchStatus.NOT_ACTIVE ? (
|
||||
<PostsDemo posts={posts} featuredPost={posts[0]} />
|
||||
) : (
|
||||
<SearchContainer />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,3 +1,169 @@
|
||||
export default function SearchPage() {
|
||||
return <div>Search</div>
|
||||
import { useSearch, useSearchGeneric } from '@/hooks/useSearch'
|
||||
import {
|
||||
UnbodyGoogleDoc,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
import unbodyApi from '@/services/unbody.service'
|
||||
import {
|
||||
SearchHook,
|
||||
SearchHookDataPayload,
|
||||
SearchResultItem,
|
||||
SearchResults,
|
||||
} from '@/types/data.types'
|
||||
import {
|
||||
createMinimizedSearchText,
|
||||
extractQueryFromQuery,
|
||||
extractTopicsFromQuery,
|
||||
} from '@/utils/search.utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface SearchPageProps {
|
||||
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||
blocks: SearchResultItem<UnbodyTextBlock | UnbodyImageBlock>[]
|
||||
}
|
||||
|
||||
export default function SearchPage({
|
||||
articles: initialArticles = [],
|
||||
blocks: initialBlocks = [],
|
||||
}: SearchPageProps) {
|
||||
const router = useRouter()
|
||||
const hasUpdated = useRef(false)
|
||||
const {
|
||||
query: { query = '', topics = [] },
|
||||
} = router
|
||||
|
||||
const articles = useSearchGeneric<UnbodyGoogleDoc>(initialArticles)
|
||||
const blocks = useSearchGeneric<UnbodyTextBlock | UnbodyImageBlock>(
|
||||
initialBlocks,
|
||||
)
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
return () => {
|
||||
setMounted(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
if (query.length > 0 || topics.length > 0) {
|
||||
const serchArgs = [
|
||||
extractQueryFromQuery(router.query),
|
||||
extractTopicsFromQuery(router.query),
|
||||
]
|
||||
articles.search(...(serchArgs as [string, string[]]))
|
||||
blocks.search(...(serchArgs as [string, string[]]))
|
||||
} else {
|
||||
articles.reset(initialArticles)
|
||||
blocks.reset(initialBlocks)
|
||||
}
|
||||
} else {
|
||||
hasUpdated.current = true
|
||||
}
|
||||
}, [mounted, router.query])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section>
|
||||
<strong>Related articles</strong>
|
||||
<hr />
|
||||
<div>
|
||||
{articles.loading && <div>...</div>}
|
||||
{!articles.error && articles.data && articles.data.length > 0 ? (
|
||||
articles.data.map((article: SearchResultItem<UnbodyGoogleDoc>) => (
|
||||
<div key={article.doc.remoteId}>
|
||||
<h2>{article.doc.title}</h2>
|
||||
<p>{article.doc.summary}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>Nothing found</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<br />
|
||||
<section>
|
||||
<strong>Related content blocks</strong>
|
||||
<hr />
|
||||
<div>
|
||||
{blocks.loading && <div>...</div>}
|
||||
{!blocks.error && blocks.data && blocks.data.length > 0 ? (
|
||||
blocks.data.map(
|
||||
(block: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>) => {
|
||||
if (!block.doc.document || !block.doc.document[0]) return null
|
||||
|
||||
let refArticle = null
|
||||
if (UnbodyGraphQl.UnbodyDocumentTypeNames.GoogleDoc) {
|
||||
refArticle = block.doc.document[0]
|
||||
}
|
||||
|
||||
switch (block.doc.__typename) {
|
||||
case UnbodyGraphQl.UnbodyDocumentTypeNames.TextBlock:
|
||||
return (
|
||||
<div key={block.doc.remoteId}>
|
||||
{refArticle && (
|
||||
<h3>
|
||||
<Link href={`/articles/${refArticle.remoteId}`}>
|
||||
{refArticle.title}
|
||||
</Link>
|
||||
</h3>
|
||||
)}
|
||||
{
|
||||
<p
|
||||
dangerouslySetInnerHTML={{ __html: block.doc.html }}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock: {
|
||||
return (
|
||||
<div key={block.doc.remoteId}>
|
||||
{refArticle && (
|
||||
<h3>
|
||||
<Link href={`/articles/${refArticle.remoteId}`}>
|
||||
{refArticle.title}
|
||||
</Link>
|
||||
</h3>
|
||||
)}
|
||||
<Image
|
||||
title={block.doc.alt}
|
||||
src={block.doc.url}
|
||||
width={block.doc.width}
|
||||
height={block.doc.height}
|
||||
alt={block.doc.alt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
) : (
|
||||
<div>Nothing found</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const { data: articles = [] } = await unbodyApi.searchArticles()
|
||||
const { data: blocks = [] } = await unbodyApi.serachBlocks()
|
||||
|
||||
return {
|
||||
props: {
|
||||
articles,
|
||||
blocks,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -7,3 +7,13 @@ export const GetGoogleDocQuery = (args: UnbodyGetFilters) => (q: string) => {
|
||||
if (Object.keys(args).length === 0) return GetQuery(`GoogleDoc{ ${q} }`)
|
||||
return GetQuery(`GoogleDoc(${parseFilterArgs(args)}){ ${q} }`)
|
||||
}
|
||||
|
||||
export const GetTextBlockQuery = (args: UnbodyGetFilters) => (q: string) => {
|
||||
if (Object.keys(args).length === 0) return `TextBlock{ ${q} }`
|
||||
return `TextBlock(${parseFilterArgs(args)}){ ${q} }`
|
||||
}
|
||||
|
||||
export const GetImageBlockQuery = (args: UnbodyGetFilters) => (q: string) => {
|
||||
if (Object.keys(args).length === 0) return `ImageBlock{ ${q} }`
|
||||
return `ImageBlock(${parseFilterArgs(args)}){ ${q} }`
|
||||
}
|
||||
|
41
src/queries/searchBlocks.ts
Normal file
41
src/queries/searchBlocks.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
GetGoogleDocQuery,
|
||||
GetImageBlockQuery,
|
||||
GetQuery,
|
||||
GetTextBlockQuery,
|
||||
} from '.'
|
||||
import { UnbodyGetFilters } from '../lib/unbody/unbody.types'
|
||||
|
||||
const defaultArgs: UnbodyGetFilters = {}
|
||||
|
||||
export const getSearchBlocksQuery = (args: UnbodyGetFilters = defaultArgs) =>
|
||||
GetQuery(
|
||||
[
|
||||
GetTextBlockQuery(args)(`
|
||||
__typename
|
||||
text
|
||||
tagName
|
||||
document{
|
||||
...on GoogleDoc{
|
||||
title
|
||||
remoteId
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`),
|
||||
GetImageBlockQuery(args)(`
|
||||
__typename
|
||||
url
|
||||
width
|
||||
height
|
||||
alt
|
||||
document{
|
||||
...on GoogleDoc{
|
||||
title
|
||||
remoteId
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`),
|
||||
].join(' '),
|
||||
)
|
@ -1,9 +1,13 @@
|
||||
class SearchService {
|
||||
constructor() {}
|
||||
|
||||
searchArticles = (query: string, tags: string[]) => {
|
||||
return fetch(`/api/search?q=${query}&tags=${tags.join(',')}`).then((res) =>
|
||||
res.json(),
|
||||
)
|
||||
return fetch(`/api/search?q=${query}&tags=${tags.join(',')}`)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return { data: null, errors: JSON.stringify(e) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,9 @@ import {
|
||||
UnbodyGoogleDoc,
|
||||
UnbodyGraphQlResponseGoogleDoc,
|
||||
UnbodyGetFilters,
|
||||
UnbodyImageBlock,
|
||||
UnbodyTextBlock,
|
||||
UnbodyGraphQlResponseBlocks,
|
||||
} from '@/lib/unbody/unbody.types'
|
||||
|
||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||
@ -17,6 +20,7 @@ import {
|
||||
GlobalSearchResponse,
|
||||
SearchResultItem,
|
||||
} from '@/types/data.types'
|
||||
import { getSearchBlocksQuery } from '@/queries/searchBlocks'
|
||||
|
||||
const { UNBODY_API_KEY, UNBODY_LPE_PROJECT_ID } = process.env
|
||||
|
||||
@ -25,6 +29,17 @@ type HomepagePost = Pick<
|
||||
'title' | 'summary' | 'tags' | 'modifiedAt' | 'subtitle' | 'blocks'
|
||||
>
|
||||
|
||||
type UnbodyDocTypes = UnbodyGoogleDoc | UnbodyImageBlock | UnbodyTextBlock
|
||||
|
||||
const mapSearchResultItem = <T extends UnbodyDocTypes>(
|
||||
q: string,
|
||||
tags: string[],
|
||||
item: T,
|
||||
): SearchResultItem<T> => ({
|
||||
doc: item,
|
||||
score: q.length > 0 || tags.length > 0 ? item._additional.certainty : 0,
|
||||
})
|
||||
|
||||
class UnbodyService extends UnbodyClient {
|
||||
constructor() {
|
||||
super(UNBODY_API_KEY as string, UNBODY_LPE_PROJECT_ID as string)
|
||||
@ -90,6 +105,39 @@ class UnbodyService extends UnbodyClient {
|
||||
.catch((e) => this.handleResponse(null, e))
|
||||
}
|
||||
|
||||
serachBlocks = async (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
): Promise<
|
||||
ApiResponse<SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]>
|
||||
> => {
|
||||
const query = getSearchBlocksQuery({
|
||||
...(q.trim().length > 0
|
||||
? {
|
||||
nearText: {
|
||||
concepts: [q, ...tags],
|
||||
certainty: 0.75,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
console.log(query)
|
||||
|
||||
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))
|
||||
.sort((a, b) => b.score - a.score),
|
||||
)
|
||||
})
|
||||
.catch((e) => this.handleResponse([], e))
|
||||
}
|
||||
|
||||
searchArticles = (
|
||||
q: string = '',
|
||||
tags: string[] = [],
|
||||
@ -115,11 +163,10 @@ class UnbodyService extends UnbodyClient {
|
||||
{}),
|
||||
})
|
||||
|
||||
console.log(q, tags, q.length, tags.length)
|
||||
|
||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||
.then(({ data }) => {
|
||||
if (!data) return this.handleResponse([], 'No data')
|
||||
if (!data || !data.Get.GoogleDoc)
|
||||
return this.handleResponse([], 'No data')
|
||||
return this.handleResponse(
|
||||
data.Get.GoogleDoc.map((item) => ({
|
||||
doc: item,
|
||||
|
@ -23,3 +23,27 @@ export type GlobalSearchResponse = {
|
||||
posts: ArticlePostData[]
|
||||
blocks: Array<SearchResultItem<UnbodyTextBlock | UnbodyImageBlock>>
|
||||
}
|
||||
|
||||
export type SearchHookDataPayload = {
|
||||
articles: SearchResultItem<UnbodyGoogleDoc>[]
|
||||
blocks: SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>[]
|
||||
}
|
||||
|
||||
export type SearchResults = {
|
||||
articles: SearchHook<UnbodyGoogleDoc>
|
||||
blocks: SearchHook<UnbodyImageBlock | UnbodyTextBlock>
|
||||
search: (query: string, tags: string[]) => Promise<void>
|
||||
reset: (initialData: SearchHookDataPayload) => void
|
||||
}
|
||||
|
||||
export type SearchResultsItemTypes =
|
||||
| SearchResultItem<UnbodyGoogleDoc>
|
||||
| SearchResultItem<UnbodyImageBlock | UnbodyTextBlock>
|
||||
|
||||
export type SearchHook<T> = {
|
||||
data: SearchResultItem<T>[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
search: (query: string, tags: string[]) => Promise<SearchResultItem<T>[]>
|
||||
reset: (initialData: SearchResultItem<T>[]) => void
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { SearchResultItem } from './data.types'
|
||||
|
||||
export enum ESearchScope {
|
||||
GLOBAL = 'global',
|
||||
ARTICLE = 'article',
|
||||
|
37
src/utils/search.utils.ts
Normal file
37
src/utils/search.utils.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { SearchResultItem } from '@/types/data.types'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
export const extractTopicsFromQuery = (query: ParsedUrlQuery): string[] => {
|
||||
return query.topics ? (query.topics as string).split(',') : []
|
||||
}
|
||||
|
||||
export const addTopicsToQuery = (topics: string[]) => {
|
||||
return {
|
||||
...(topics.length > 0 && { topics: topics.join(',') }),
|
||||
}
|
||||
}
|
||||
|
||||
export const extractQueryFromQuery = (queryObj: ParsedUrlQuery): string => {
|
||||
return (queryObj.query as string) || ''
|
||||
}
|
||||
|
||||
export const addQueryToQuery = (query: string) => {
|
||||
return {
|
||||
...(query && query.length > 0 && { query }),
|
||||
}
|
||||
}
|
||||
|
||||
export const createMinimizedSearchText = (
|
||||
query: string,
|
||||
filterTags: string[],
|
||||
) => {
|
||||
let txt = ''
|
||||
if (query !== undefined && query.length > 0) {
|
||||
txt += `<span>${query}</span>`
|
||||
}
|
||||
if (filterTags.length > 0) {
|
||||
if (txt.length > 0) txt += '<b> . </b>'
|
||||
txt += `${filterTags.map((t) => `<small>[${t}]</small>`).join(' ')}`
|
||||
}
|
||||
return txt
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user