Commit c7cb8410 authored by tom's avatar tom

search: add simple match item to suggest

parent de3956e2
...@@ -13,20 +13,20 @@ export interface SearchResultAddressOrContract { ...@@ -13,20 +13,20 @@ export interface SearchResultAddressOrContract {
type: 'address' | 'contract'; type: 'address' | 'contract';
name: string | null; name: string | null;
address: string; address: string;
url: string; url?: string; // not used by the frontend, we build the url ourselves
} }
export interface SearchResultBlock { export interface SearchResultBlock {
type: 'block'; type: 'block';
block_number: number; block_number: number | string;
block_hash: string; block_hash: string;
url: string; url?: string; // not used by the frontend, we build the url ourselves
} }
export interface SearchResultTx { export interface SearchResultTx {
type: 'transaction'; type: 'transaction';
tx_hash: string; tx_hash: string;
url: string; url?: string; // not used by the frontend, we build the url ourselves
} }
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx; export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx;
......
...@@ -132,3 +132,39 @@ test('search by tx hash +@mobile', async({ mount, page }) => { ...@@ -132,3 +132,39 @@ test('search by tx hash +@mobile', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } });
}); });
test('search with simple match', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`;
const API_CHECK_REDIRECT_URL = buildApiUrl('search_check_redirect') + `?q=${ searchMock.tx1.tx_hash }`;
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.tx1,
],
}),
}));
await page.route(API_CHECK_REDIRECT_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
parameter: searchMock.tx1.tx_hash,
redirect: true,
type: 'transaction',
}),
}));
await mount(
<TestApp>
<SearchBar/>
</TestApp>,
);
await page.getByPlaceholder(/search/i).type(searchMock.tx1.tx_hash);
await page.waitForResponse(API_URL);
const resultText = page.getByText('Found 1 matching result');
expect(resultText).toBeTruthy();
const links = page.getByText(searchMock.tx1.tx_hash);
await expect(links).toHaveCount(1);
});
...@@ -21,7 +21,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -21,7 +21,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const menuWidth = React.useRef<number>(0); const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { searchTerm, handleSearchTermChange, query } = useSearchQuery(); const { searchTerm, handleSearchTermChange, query, redirectCheckQuery } = useSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -98,7 +98,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -98,7 +98,7 @@ const SearchBar = ({ isHomepage }: Props) => {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }> <PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }>
<PopoverBody py={ 6 }> <PopoverBody py={ 6 }>
<SearchBarSuggest query={ query } searchTerm={ searchTerm }/> <SearchBarSuggest query={ query } redirectCheckQuery={ redirectCheckQuery } searchTerm={ searchTerm }/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
......
import { Text } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import _uniqBy from 'lodash/uniqBy';
import React from 'react'; import React from 'react';
import type { SearchResult } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultItem } from 'types/api/search';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -11,32 +12,84 @@ import type { Props as PaginationProps } from 'ui/shared/Pagination'; ...@@ -11,32 +12,84 @@ import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SearchBarSuggestItem from './SearchBarSuggestItem'; import SearchBarSuggestItem from './SearchBarSuggestItem';
const getUniqueIdentifier = (item: SearchResultItem) => {
switch (item.type) {
case 'contract':
case 'address': {
return item.address;
}
case 'transaction': {
return item.tx_hash;
}
case 'block': {
return item.block_hash || item.block_number;
}
case 'token': {
return item.address;
}
}
};
interface Props { interface Props {
query: UseQueryResult<SearchResult> & { query: UseQueryResult<SearchResult> & {
pagination: PaginationProps; pagination: PaginationProps;
}; };
redirectCheckQuery: UseQueryResult<SearchRedirectResult>;
searchTerm: string; searchTerm: string;
} }
const SearchBarSuggest = ({ query, searchTerm }: Props) => { const SearchBarSuggest = ({ query, redirectCheckQuery, searchTerm }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const simpleMatch: SearchResultItem | undefined = React.useMemo(() => {
if (!redirectCheckQuery.data || !redirectCheckQuery.data.redirect || !redirectCheckQuery.data.parameter) {
return;
}
switch (redirectCheckQuery.data?.type) {
case 'address': {
return {
type: 'address',
name: '',
address: redirectCheckQuery.data.parameter,
};
}
case 'transaction': {
return {
type: 'transaction',
tx_hash: redirectCheckQuery.data.parameter,
};
}
}
}, [ redirectCheckQuery.data ]);
const items = React.useMemo(() => {
return _uniqBy(
[
simpleMatch,
...(query.data?.items || []),
].filter(Boolean),
getUniqueIdentifier,
);
}, [ query.data?.items, simpleMatch ]);
const content = (() => { const content = (() => {
if (query.isLoading) { if (query.isLoading && !simpleMatch) {
return <ContentLoader text="We are searching, please wait... "/>; return <ContentLoader text="We are searching, please wait... " fontSize="sm"/>;
} }
if (query.isError) { if (query.isError && !simpleMatch) {
return <Text>Something went wrong. Try refreshing the page or come back later.</Text>; return <Text>Something went wrong. Try refreshing the page or come back later.</Text>;
} }
const num = query.data.next_page_params ? '50+' : query.data.items.length; const num = query.data?.next_page_params ? '50+' : items.length;
const resultText = query.data.items.length > 1 || query.pagination.page > 1 ? 'results' : 'result'; const resultText = items.length > 1 || query.pagination.page > 1 ? 'results' : 'result';
return ( return (
<> <>
<Text fontWeight={ 500 } fontSize="sm">Found <Text fontWeight={ 700 } as="span">{ num }</Text> matching { resultText }</Text> <Text fontWeight={ 500 } fontSize="sm">Found <Text fontWeight={ 700 } as="span">{ num }</Text> matching { resultText }</Text>
{ query.data.items.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm }/>) } { items.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm }/>) }
{ query.isLoading && <ContentLoader text="We are still searching, please wait... " fontSize="sm" mt={ 5 }/> }
</> </>
); );
})(); })();
......
...@@ -24,7 +24,10 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -24,7 +24,10 @@ export default function useSearchQuery(isSearchPage = false) {
const redirectCheckQuery = useApiQuery('search_check_redirect', { const redirectCheckQuery = useApiQuery('search_check_redirect', {
queryParams: { q: q.current }, queryParams: { q: q.current },
queryOptions: { enabled: isSearchPage && Boolean(q) }, // on search result page we check redirect only once on mount
// on pages with regular search bar we check redirect on every search term change
// in order to prepend its result to suggest list since this resource is much faster than regular search
queryOptions: { enabled: Boolean(isSearchPage ? q : debouncedSearchTerm) },
}); });
useUpdateValueEffect(() => { useUpdateValueEffect(() => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment