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 { export interface SearchResultToken {
type: 'token'; type: 'token';
...@@ -9,8 +9,8 @@ export interface SearchResultToken { ...@@ -9,8 +9,8 @@ export interface SearchResultToken {
address_url: string; address_url: string;
} }
export interface SearchResultAddress { export interface SearchResultAddressOrContract {
type: 'address'; type: 'address' | 'contract';
name: string | null; name: string | null;
address: string; address: string;
url: string; url: string;
...@@ -29,7 +29,7 @@ export interface SearchResultTx { ...@@ -29,7 +29,7 @@ export interface SearchResultTx {
url: string; url: string;
} }
export type SearchResultItem = SearchResultToken | SearchResultAddress | SearchResultBlock | SearchResultTx; export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx;
export interface SearchResult { export interface SearchResult {
items: Array<SearchResultItem>; items: Array<SearchResultItem>;
......
...@@ -14,50 +14,6 @@ type Props = { ...@@ -14,50 +14,6 @@ type Props = {
isHomepage?: boolean; 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 SearchBar = ({ isHomepage, withShadow }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const inputRef = React.useRef<HTMLFormElement>(null); const inputRef = React.useRef<HTMLFormElement>(null);
...@@ -65,7 +21,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -65,7 +21,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
const menuWidth = React.useRef<number>(0); const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { searchTerm, handleSearchTermChange } = useSearchQuery(); const { searchTerm, handleSearchTermChange, query } = useSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -99,7 +55,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -99,7 +55,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
return ( return (
<Popover <Popover
isOpen={ isOpen } isOpen={ isOpen && searchTerm.trim().length > 0 }
autoFocus={ false } autoFocus={ false }
onClose={ onClose } onClose={ onClose }
placement="bottom-start" placement="bottom-start"
...@@ -118,7 +74,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -118,7 +74,7 @@ const SearchBar = ({ isHomepage, withShadow }: 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 display="flex" flexDirection="column" rowGap="6"> <PopoverBody display="flex" flexDirection="column" rowGap="6">
<SearchBarSuggest data={{ items: data, next_page_params: null }}/> <SearchBarSuggest query={ query }/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
......
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import _groupBy from 'lodash/groupBy'; import _groupBy from 'lodash/groupBy';
import React from 'react'; import React from 'react';
...@@ -9,7 +10,7 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -9,7 +10,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import SearchBarSuggestItem from './SearchBarSuggestItem'; import SearchBarSuggestItem from './SearchBarSuggestItem';
interface Props { interface Props {
data: SearchResult; query: UseQueryResult<SearchResult>;
} }
interface Group { interface Group {
...@@ -22,24 +23,41 @@ const GROUPS: Array<Group> = [ ...@@ -22,24 +23,41 @@ const GROUPS: Array<Group> = [
{ type: 'token', title: 'Tokens' }, { type: 'token', title: 'Tokens' },
{ type: 'address', title: 'Address' }, { type: 'address', title: 'Address' },
{ type: 'transaction', title: 'Transactions' }, { type: 'transaction', title: 'Transactions' },
{ type: 'contract', title: 'Contracts' },
]; ];
const SearchBarSuggest = ({ data }: Props) => { const SearchBarSuggest = ({ query }: Props) => {
const isMobile = useIsMobile(); 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 ( return (
<> <Box>
{ Object.entries(groupedData).map(([ group, data ]) => { { content }
const groupName = GROUPS.find(({ type }) => type === group)?.title; </Box>
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>
);
}) }
</>
); );
}; };
......
...@@ -21,6 +21,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => { ...@@ -21,6 +21,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
case 'token': { case 'token': {
return link('token_index', { hash: data.address }); return link('token_index', { hash: data.address });
} }
case 'contract':
case 'address': { case 'address': {
return link('address_index', { id: data.address }); return link('address_index', { id: data.address });
} }
...@@ -51,6 +52,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => { ...@@ -51,6 +52,7 @@ const SearchBarSuggestItem = ({ data, isMobile }: Props) => {
</> </>
); );
} }
case 'contract':
case 'address': { case 'address': {
return ( return (
<Address> <Address>
......
import { useQuery } from '@tanstack/react-query';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -5,16 +6,62 @@ import type { SearchResult } from 'types/api/search'; ...@@ -5,16 +6,62 @@ import type { SearchResult } from 'types/api/search';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; 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() { export default function useSearchQuery() {
const [ searchTerm, setSearchTerm ] = React.useState(''); const [ searchTerm, setSearchTerm ] = React.useState('');
const searchTermRef = React.useRef('');
const abortControllerRef = React.useRef<AbortController>(); const abortControllerRef = React.useRef<AbortController>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const query = useDebouncedQuery<SearchResult, ResourceError>( const debouncedSearchTerm = useDebounce(searchTerm, 300);
[ 'search', searchTerm ],
const query = useQuery<unknown, ResourceError, SearchResult>(
getResourceKey('search', { queryParams: { q: debouncedSearchTerm } }),
() => { () => {
if (abortControllerRef.current) { if (abortControllerRef.current) {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
...@@ -23,18 +70,20 @@ export default function useSearchQuery() { ...@@ -23,18 +70,20 @@ export default function useSearchQuery() {
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
return apiFetch<'search', SearchResult>('search', { return apiFetch<'search', SearchResult>('search', {
queryParams: { q: searchTermRef.current }, queryParams: { q: debouncedSearchTerm },
fetchParams: { signal: abortControllerRef.current.signal }, 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 handleSearchTermChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value; setSearchTerm(event.target.value);
setSearchTerm(value);
searchTermRef.current = value;
}, []); }, []);
return { 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