init serach fn and context provider
This commit is contained in:
parent
0babbafd35
commit
2aa98625cf
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { useArticlesSearch } from '@/hooks/useSearch'
|
||||
|
||||
type SearchContainerProps = {}
|
||||
|
||||
export const SearchContainer = (props: SearchContainerProps) => {
|
||||
return <div>Search result container...</div>
|
||||
}
|
|
@ -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)
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default function SearchPage() {
|
||||
return <div>Search</div>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
`)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
@ -2,3 +2,10 @@ export enum ESearchScope {
|
|||
GLOBAL = 'global',
|
||||
ARTICLE = 'article',
|
||||
}
|
||||
|
||||
export enum ESearchStatus {
|
||||
SEARCHING = 'searching',
|
||||
IDLE = 'idle',
|
||||
ERROR = 'error',
|
||||
NOT_ACTIVE = 'not_active',
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue