Commit dbd3f2fb authored by tom's avatar tom

add api

parent f4f1b772
...@@ -19,6 +19,7 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction ...@@ -19,6 +19,7 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl'; import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponse } from 'types/api/log'; import type { LogsResponse } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult } from 'types/api/search';
import type { Stats, Charts, HomeStats } from 'types/api/stats'; import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
...@@ -179,6 +180,11 @@ export const RESOURCES = { ...@@ -179,6 +180,11 @@ export const RESOURCES = {
path: '/api/v2/config/json-rpc-url', path: '/api/v2/config/json-rpc-url',
}, },
// SEARCH
search: {
path: '/api/v2/search',
},
// DEPRECATED // DEPRECATED
old_api: { old_api: {
path: '/api', path: '/api',
...@@ -250,6 +256,7 @@ Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : ...@@ -250,6 +256,7 @@ Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse : Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'search' ? SearchResult :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -6,18 +6,18 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -6,18 +6,18 @@ import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl'; import buildUrl from './buildUrl';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
import type { ResourceError, ApiResource } from './resources'; import type { ApiResource } from './resources';
export interface Params { export interface Params {
pathParams?: Record<string, string | undefined>; pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>; queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>; fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
} }
export default function useApiFetch() { export default function useApiFetch() {
const fetch = useFetch(); const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = ResourceError>( return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = unknown>(
resourceName: R, resourceName: R,
{ pathParams, queryParams, fetchParams }: Params = {}, { pathParams, queryParams, fetchParams }: Params = {},
) => { ) => {
......
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,
);
}
...@@ -9,6 +9,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -9,6 +9,7 @@ import type { ResourceError } from 'lib/api/resources';
export interface Params { export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown>;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import type { ChangeEvent, FormEvent, FocusEvent } from 'react'; import type { FormEvent, FocusEvent } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -7,6 +7,7 @@ import link from 'lib/link/link'; ...@@ -7,6 +7,7 @@ import link from 'lib/link/link';
import SearchBarInput from './SearchBarInput'; import SearchBarInput from './SearchBarInput';
import SearchBarSuggest from './SearchBarSuggest'; import SearchBarSuggest from './SearchBarSuggest';
import useSearchQuery from './useSearchQuery';
type Props = { type Props = {
withShadow?: boolean; withShadow?: boolean;
...@@ -58,22 +59,19 @@ const data = [ ...@@ -58,22 +59,19 @@ const data = [
]; ];
const SearchBar = ({ isHomepage, withShadow }: Props) => { const SearchBar = ({ isHomepage, withShadow }: Props) => {
const [ value, setValue ] = React.useState('');
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const inputRef = React.useRef<HTMLFormElement>(null); const inputRef = React.useRef<HTMLFormElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null); const menuRef = React.useRef<HTMLDivElement>(null);
const menuWidth = React.useRef<number>(0); const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const { searchTerm, handleSearchTermChange } = useSearchQuery();
setValue(event.target.value);
}, []);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const url = link('search_results', undefined, { q: value }); const url = link('search_results', undefined, { q: searchTerm });
window.location.assign(url); window.location.assign(url);
}, [ value ]); }, [ searchTerm ]);
const handleFocus = React.useCallback(() => { const handleFocus = React.useCallback(() => {
onOpen(); onOpen();
...@@ -110,7 +108,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => { ...@@ -110,7 +108,7 @@ const SearchBar = ({ isHomepage, withShadow }: Props) => {
<PopoverTrigger> <PopoverTrigger>
<SearchBarInput <SearchBarInput
ref={ inputRef } ref={ inputRef }
onChange={ handleChange } onChange={ handleSearchTermChange }
onSubmit={ handleSubmit } onSubmit={ handleSubmit }
onFocus={ handleFocus } onFocus={ handleFocus }
onBlur={ handleBlur } onBlur={ handleBlur }
......
import type { ChangeEvent } from 'react';
import React from 'react';
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';
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 ],
() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
return apiFetch<'search', SearchResult>('search', {
queryParams: { q: searchTermRef.current },
fetchParams: { signal: abortControllerRef.current.signal },
});
},
300,
{ enabled: searchTerm.trim().length > 0 },
);
const handleSearchTermChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchTerm(value);
searchTermRef.current = value;
}, []);
return {
searchTerm,
handleSearchTermChange,
query,
};
}
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