mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
add real api
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user