init serach fn and context provider

This commit is contained in:
amirhouieh 2023-05-08 19:02:22 +02:00 committed by Jinho Jang
parent 0babbafd35
commit 2aa98625cf
15 changed files with 235 additions and 46 deletions

View File

@ -7,10 +7,11 @@ import {
import styles from './Search.module.css'
import { SearchbarContainer } from '@/components/Searchbar/SearchbarContainer'
import { copyConfigs } from '@/configs/copy.configs'
import { ESearchScope } from '@/types/ui.types'
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'
export type SearchbarProps = {
searchScope?: ESearchScope
@ -19,21 +20,21 @@ export type SearchbarProps = {
export default function Searchbar(props: SearchbarProps) {
const { searchScope = ESearchScope.GLOBAL } = props
const [active, setActive] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const { exec } = useSearchContext()
// const [loading, setLoading] = useState(false)
// const [error, setError] = useState(false)
const [query, setQuery] = useState<string>('')
const [filterTags, setFilterTags] = useState<string[]>([])
const validateSearch = useCallback(() => {
return query.length > 0 || filterTags.length > 0
}, [query, filterTags])
const isValidSearchInput = () =>
(query && query.length > 0) || filterTags.length > 0
const performSearch = useCallback(() => {
if (!validateSearch()) return
}, [validateSearch])
const performSearch = async () => {
if (!isValidSearchInput()) return
exec(query, filterTags)
}
const performClear = useCallback(() => {
// TODO: clear input.value seems to be not working. When set to undefined, the input value is still there.
@ -41,10 +42,6 @@ export default function Searchbar(props: SearchbarProps) {
setFilterTags([])
}, [setQuery, setFilterTags])
useEffect(() => {
performSearch()
}, [filterTags, performSearch])
const handleTagClick = (tag: string) => {
let newSelectedTags = [...filterTags]
if (newSelectedTags.includes(tag)) {
@ -73,7 +70,7 @@ export default function Searchbar(props: SearchbarProps) {
return txt
}
const isCollapsed = validateSearch() && !active
const isCollapsed = isValidSearchInput() && !active
const placeholder =
searchScope === ESearchScope.GLOBAL
@ -88,6 +85,7 @@ export default function Searchbar(props: SearchbarProps) {
placeholder={placeholder}
value={query as string}
onFocus={() => setActive(true)}
onKeyDown={handleEnter}
onChange={(e) => {
setQuery(e.target.value)
}}
@ -96,10 +94,10 @@ export default function Searchbar(props: SearchbarProps) {
<IconButton
className={styles.searchButton}
onClick={() =>
validateSearch() ? performClear() : performSearch()
isValidSearchInput() ? performClear() : performSearch()
}
>
{validateSearch() ? <CloseIcon /> : <SearchIcon />}
{isValidSearchInput() ? <CloseIcon /> : <SearchIcon />}
</IconButton>
</div>
</SearchBox>

View File

@ -0,0 +1,7 @@
import { useArticlesSearch } from '@/hooks/useSearch'
type SearchContainerProps = {}
export const SearchContainer = (props: SearchContainerProps) => {
return <div>Search result container...</div>
}

View File

@ -0,0 +1,47 @@
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)

21
src/hooks/useSearch.ts Normal file
View File

@ -0,0 +1,21 @@
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
import searchApi from '@/services/search.service'
import { SearchResultItem } from '@/types/data.types'
import { useState } from 'react'
export const useArticlesSearch = () => {
const [data, setData] = useState<SearchResultItem<UnbodyGoogleDoc>[] | null>(
null,
)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const search = async (query: string, tags: string[]) => {
setLoading(true)
const result = await searchApi.searchArticles(query, tags)
setData(result)
setLoading(false)
}
return { data, loading, error, search }
}

View File

@ -283,8 +283,8 @@ export namespace UnbodyGraphQl {
export interface WhereOperandsInpObj {
operator?: WhereOperatorEnum
path: [String]
operands?: [WhereOperandsInpObj]
path: string[]
operands?: WhereOperandsInpObj[]
valueGeoRange?: WhereGeoRangeInpObj
valueNumber?: number
valueBoolean?: boolean
@ -315,7 +315,7 @@ export namespace UnbodyGraphQl {
}
export interface WhereInpObj {
path: string[]
path?: string[]
valueInt?: number
valueNumber?: number
valueGeoRange?: WhereGeoRangeInpObj

View File

@ -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 { SearchContextProvider } from '@/context/SearchContext'
type NextLayoutComponentType<P = {}> = NextComponentType<
NextPageContext,
@ -50,7 +51,9 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
}
`}
/>
{getLayout(<Component {...pageProps} />)}
<SearchContextProvider>
{getLayout(<Component {...pageProps} />)}
</SearchContextProvider>
</ThemeProvider>
)
}

View File

@ -1,15 +0,0 @@
import unbody, { getHomepagePosts } from '@/services/unbody.service'
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<any>,
) {
try {
const data = await getHomepagePosts()
res.status(200).json(data)
} catch (e: any) {
console.log(e)
res.status(e.response.statusCode || 500).send(e.message)
}
}

20
src/pages/api/search.ts Normal file
View File

@ -0,0 +1,20 @@
import api from '@/services/unbody.service'
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<any>,
) {
const {
query: { q = '', tags: tagsString = '' },
} = req
const tags =
typeof tagsString === 'string'
? tagsString
.split(',')
.map((tag: string) => tag.trim())
.filter((t) => t.length > 0)
: undefined
const response = await api.searchArticles(q as string, tags)
res.status(200).json(response)
}

View File

@ -1,7 +1,10 @@
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'
import { GetStaticProps } from 'next'
type Props = {
@ -10,9 +13,15 @@ type Props = {
}
export default function Home({ posts }: Props) {
const { status } = useSearchContext()
return (
<>
<PostsDemo posts={posts} featuredPost={posts[0]} />
{status === ESearchStatus.NOT_ACTIVE ? (
<PostsDemo posts={posts} featuredPost={posts[0]} />
) : (
<SearchContainer />
)}
</>
)
}

3
src/pages/search.tsx Normal file
View File

@ -0,0 +1,3 @@
export default function SearchPage() {
return <div>Search</div>
}

View File

@ -0,0 +1,16 @@
import { GetGoogleDocQuery } from '.'
import { UnbodyGetFilters } from '../lib/unbody/unbody.types'
const defaultArgs: UnbodyGetFilters = {}
export const getSearchArticlesQuery = (args: UnbodyGetFilters = defaultArgs) =>
GetGoogleDocQuery(args)(`
remoteId
title
summary
tags
modifiedAt
_additional{
certainty
}
`)

View File

@ -0,0 +1,11 @@
class SearchService {
constructor() {}
searchArticles = (query: string, tags: string[]) => {
return fetch(`/api/search?q=${query}&tags=${tags.join(',')}`).then((res) =>
res.json(),
)
}
}
const searchApi = new SearchService()
export default searchApi

View File

@ -10,6 +10,13 @@ import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
import { getArticlePostQuery } from '@/queries/getPost'
import { getHomePagePostsQuery } from '@/queries/getPosts'
import { getAllPostsSlugQuery } from '@/queries/getPostsSlugs'
import { getSearchArticlesQuery } from '@/queries/searchArticles'
import {
ApiResponse,
GlobalSearchResponse,
SearchResultItem,
} from '@/types/data.types'
const { UNBODY_API_KEY, UNBODY_LPE_PROJECT_ID } = process.env
@ -18,12 +25,7 @@ type HomepagePost = Pick<
'title' | 'summary' | 'tags' | 'modifiedAt' | 'subtitle' | 'blocks'
>
type ApiResponse<T> = {
data: T
errors: any
}
class ApiService extends UnbodyClient {
class UnbodyService extends UnbodyClient {
constructor() {
super(UNBODY_API_KEY as string, UNBODY_LPE_PROJECT_ID as string)
}
@ -87,7 +89,48 @@ class ApiService extends UnbodyClient {
})
.catch((e) => this.handleResponse(null, e))
}
searchArticles = (
q: string = '',
tags: string[] = [],
): Promise<ApiResponse<SearchResultItem<UnbodyGoogleDoc>[]>> => {
const query = getSearchArticlesQuery({
...(q.trim().length > 0
? {
nearText: {
concepts: [q],
},
}
: {}),
...((tags.length > 0 && {
where: {
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.And,
operands: tags.map((tag) => ({
path: ['tags'],
operator: UnbodyGraphQl.Filters.WhereOperatorEnum.Like,
valueString: tag,
})),
},
}) ||
{}),
})
console.log(q, tags, q.length, tags.length)
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
.then(({ data }) => {
if (!data) return this.handleResponse([], 'No data')
return this.handleResponse(
data.Get.GoogleDoc.map((item) => ({
doc: item,
score:
q.length > 0 || tags.length > 0 ? item._additional.certainty : 0,
})),
)
})
.catch((e) => this.handleResponse([], e))
}
}
const api = new ApiService()
export default api
const unbodyApi = new UnbodyService()
export default unbodyApi

View File

@ -1,6 +1,25 @@
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
import {
UnbodyGoogleDoc,
UnbodyImageBlock,
UnbodyTextBlock,
} from '@/lib/unbody/unbody.types'
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
export interface ArticlePostData extends UnbodyGoogleDoc {
toc: Array<UnbodyGraphQl.Fragments.TocItem>
}
export type ApiResponse<T> = {
data: T
errors: any
}
export type SearchResultItem<T> = {
doc: T
score: number
}
export type GlobalSearchResponse = {
posts: ArticlePostData[]
blocks: Array<SearchResultItem<UnbodyTextBlock | UnbodyImageBlock>>
}

View File

@ -2,3 +2,10 @@ export enum ESearchScope {
GLOBAL = 'global',
ARTICLE = 'article',
}
export enum ESearchStatus {
SEARCHING = 'searching',
IDLE = 'idle',
ERROR = 'error',
NOT_ACTIVE = 'not_active',
}