Commit 48a0722c authored by tom's avatar tom

add real api

parent 05178c32
import React from 'react';
export default function useDebounce(value: string, delay: number) {
const [ debouncedValue, setDebouncedValue ] = React.useState(value);
React.useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
},
[ value, delay ],
);
return debouncedValue;
}
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type {
QueryKey,
QueryFunction,
UseQueryOptions,
QueryFunctionContext } from '@tanstack/react-query';
import { useRef } from 'react';
export default function useDebouncedQuery<TQueryData, TQueryError = unknown>(
queryKey: QueryKey,
queryFn: QueryFunction<TQueryData | TQueryError>,
debounceMs: number,
remainingUseQueryOptions?: UseQueryOptions<unknown, TQueryError, TQueryData>,
) {
const timeoutRef = useRef<number>();
const queryClient = useQueryClient();
const previousQueryKeyRef = useRef<QueryKey>();
const debouncesQueryFn: (queryFnContext: QueryFunctionContext) => Promise<TQueryData | TQueryError> = (queryFnContext: QueryFunctionContext) => {
// This means the react-query is retrying the query so we should not debounce it.
if (previousQueryKeyRef.current === queryKey) {
return queryFn(queryFnContext) as Promise<TQueryData | TQueryError>;
}
// We need to cancel previous "pending" queries otherwise react-query will give us an infinite
// loading state for this key since the Promise we returned was neither resolved nor rejected.
if (previousQueryKeyRef.current) {
queryClient.cancelQueries({ queryKey: previousQueryKeyRef.current });
}
previousQueryKeyRef.current = queryKey;
window.clearTimeout(timeoutRef.current);
return new Promise((resolve, reject) => {
timeoutRef.current = window.setTimeout(async() => {
try {
const result = await queryFn(queryFnContext);
previousQueryKeyRef.current = undefined;
resolve(result as TQueryData);
} catch (error) {
reject(error as TQueryError);
}
}, debounceMs);
});
};
return useQuery<unknown, TQueryError, TQueryData>(
queryKey,
debouncesQueryFn,
remainingUseQueryOptions,
);
}
export type SearchResultType = 'token' | 'address' | 'block' | 'transaction';
export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract';
export interface SearchResultToken {
type: 'token';
......@@ -9,8 +9,8 @@ export interface SearchResultToken {
address_url: string;
}
export interface SearchResultAddress {
type: 'address';
export interface SearchResultAddressOrContract {
type: 'address' | 'contract';
name: string | null;
address: string;
url: string;
......@@ -29,7 +29,7 @@ export interface SearchResultTx {
url: string;
}
export type SearchResultItem = SearchResultToken | SearchResultAddress | SearchResultBlock | SearchResultTx;
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx;
export interface SearchResult {
items: Array<SearchResultItem>;
......
......@@ -14,50 +14,6 @@ type Props = {
isHomepage?: boolean;
}
const data = [
{
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
name: 'Toms NFT',
symbol: 'TNT',
token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
type: 'token' as const,
},
{
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken',
symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const,
},
{
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken',
symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const,
},
{
block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
block_number: 8198536,
type: 'block' as const,
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
},
{
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
type: 'address' as const,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
},
{
tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
type: 'transaction' as const,
url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
},
];
const SearchBar = ({ isHomepage, withShadow }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const inputRef = React.useRef<HTMLFormElement>(null);
......@@ -65,7 +21,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile();
const { searchTerm, handleSearchTermChange } = useSearchQuery();
const { searchTerm, handleSearchTermChange, query } = useSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
......@@ -99,7 +55,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
return (
<Popover
isOpen={ isOpen }
isOpen={ isOpen && searchTerm.trim().length > 0 }
autoFocus={ false }
onClose={ onClose }
placement="bottom-start"
......@@ -118,7 +74,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
</PopoverTrigger>
<PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }>
<PopoverBody display="flex" flexDirection="column" rowGap="6">
<SearchBarSuggest data={{ items: data, next_page_params: null }}/>
<SearchBarSuggest query={ query }/>
</PopoverBody>
</PopoverContent>
</Popover>
......
import { Box, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import _groupBy from 'lodash/groupBy';
import React from 'react';
......@@ -9,7 +10,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import SearchBarSuggestItem from './SearchBarSuggestItem';
interface Props {
data: SearchResult;
query: UseQueryResult<SearchResult>;
}
interface Group {
......@@ -22,24 +23,41 @@ const GROUPS: Array<Group> = [
{ type: 'token', title: 'Tokens' },
{ type: 'address', title: 'Address' },
{ type: 'transaction', title: 'Transactions' },
{ type: 'contract', title: 'Contracts' },
];
const SearchBarSuggest = ({ data }: Props) => {
const SearchBarSuggest = ({ query }: Props) => {
const isMobile = useIsMobile();
const groupedData = _groupBy(data.items, 'type');
const groupedData = _groupBy(query.data?.items || [], 'type');
const content = (() => {
if (query.isLoading) {
return <Box>loading...</Box>;
}
if (query.isError) {
return <Box>Something went wrong. Try refreshing the page or come back later.</Box>;
}
if (query.data.items.length === 0) {
return <Box fontWeight={ 500 }>Found <Text fontWeight={ 700 } as="span">0</Text> matching results</Box>;
}
return Object.entries(groupedData).map(([ group, data ]) => {
const groupName = GROUPS.find(({ type }) => type === group)?.title;
return (
<Box key={ group }>
<Text variant="secondary" fontSize="sm" fontWeight={ 600 } mb={ 3 }>{ groupName } ({ data.length })</Text>
{ data.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile }/>) }
</Box>
);
});
})();
return (
<>
{ Object.entries(groupedData).map(([ group, data ]) => {
const groupName = GROUPS.find(({ type }) => type === group)?.title;
return (
<Box key={ group }>
<Text variant="secondary" fontSize="sm" fontWeight={ 600 } mb={ 3 }>{ groupName } ({ data.length })</Text>
{ data.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile }/>) }
</Box>
);
}) }
</>
<Box>
{ content }
</Box>
);
};
......
......@@ -21,6 +21,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
case 'token': {
return link('token_index', { hash: data.address });
}
case 'contract':
case 'address': {
return link('address_index', { id: data.address });
}
......@@ -51,6 +52,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
</>
);
}
case 'contract':
case 'address': {
return (
<Address>
......
import { useQuery } from '@tanstack/react-query';
import type { ChangeEvent } from 'react';
import React from 'react';
......@@ -5,16 +6,62 @@ import type { SearchResult } from 'types/api/search';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import useDebouncedQuery from 'lib/hooks/useDebouncedQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
const data = [
{
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
name: 'Toms NFT',
symbol: 'TNT',
token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
type: 'token' as const,
},
{
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken',
symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const,
},
{
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken',
symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const,
},
{
block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
block_number: 8198536,
type: 'block' as const,
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
},
{
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
type: 'address' as const,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
},
{
tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
type: 'transaction' as const,
url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
},
];
export default function useSearchQuery() {
const [ searchTerm, setSearchTerm ] = React.useState('');
const searchTermRef = React.useRef('');
const abortControllerRef = React.useRef<AbortController>();
const apiFetch = useApiFetch();
const query = useDebouncedQuery<SearchResult, ResourceError>(
[ 'search', searchTerm ],
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const query = useQuery<unknown, ResourceError, SearchResult>(
getResourceKey('search', { queryParams: { q: debouncedSearchTerm } }),
() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
......@@ -23,18 +70,20 @@ export default function useSearchQuery() {
abortControllerRef.current = new AbortController();
return apiFetch<'search', SearchResult>('search', {
queryParams: { q: searchTermRef.current },
queryParams: { q: debouncedSearchTerm },
fetchParams: { signal: abortControllerRef.current.signal },
});
},
300,
{ enabled: searchTerm.trim().length > 0 },
{
enabled: debouncedSearchTerm.trim().length > 0,
initialData: {
items: data,
},
},
);
const handleSearchTermChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchTerm(value);
searchTermRef.current = value;
setSearchTerm(event.target.value);
}, []);
return {
......
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