Commit 3bd5b6cd authored by tom's avatar tom

pagination

parent cf2149ad
...@@ -19,7 +19,7 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction ...@@ -19,7 +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 { SearchResult, SearchResultFilters } 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';
...@@ -183,6 +183,18 @@ export const RESOURCES = { ...@@ -183,6 +183,18 @@ export const RESOURCES = {
// SEARCH // SEARCH
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
paginationFields: [
'address_hash' as const,
'block_hash' as const,
'holder_count' as const,
'inserted_at' as const,
'item_type' as const,
'items_count' as const,
'name' as const,
'q' as const,
'tx_hash' as const,
],
filterFields: [ 'q' ],
}, },
// DEPRECATED // DEPRECATED
...@@ -215,7 +227,8 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> ...@@ -215,7 +227,8 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance'; 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -267,5 +280,6 @@ Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : ...@@ -267,5 +280,6 @@ Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters : Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'search' ? SearchResultFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues'; import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
...@@ -43,7 +44,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -43,7 +44,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const isMounted = React.useRef(false); const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page); const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...filters, ...pageParams[page] }; const queryParams = { ...pageParams[page], ...filters };
const scrollToTop = useCallback(() => { const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 }); scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
...@@ -73,7 +74,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -73,7 +74,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
setPage(prev => prev + 1); setPage(prev => prev + 1);
const nextPageQuery = { ...router.query }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
setHasPagination(true); setHasPagination(true);
...@@ -88,7 +89,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -88,7 +89,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, resource.paginationFields, 'page'); nextPageQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page');
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = pageParams[page - 1]; const nextPageParams = pageParams[page - 1];
...@@ -103,12 +104,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -103,12 +104,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]); }, [ router, page, resource.paginationFields, resource.filterFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => { const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page');
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
setPage(1); setPage(1);
...@@ -122,10 +124,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -122,10 +124,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]); }, [ queryClient, resourceName, router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields); const newQuery = omit<typeof router.query>(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -133,6 +135,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -133,6 +135,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
} }
}); });
} }
setHasPagination(false);
router.push( router.push(
{ {
pathname: router.pathname, pathname: router.pathname,
......
import React from 'react';
// run effect only if value is updated since initial mount
const useUpdateValueEffect = (effect: () => void, value: string) => {
const mountedRef = React.useRef(false);
const valueRef = React.useRef<string>();
const isChangedRef = React.useRef(false);
React.useEffect(() => {
mountedRef.current = true;
valueRef.current = value;
return () => {
mountedRef.current = false;
valueRef.current = undefined;
isChangedRef.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (mountedRef.current && (value !== valueRef.current || isChangedRef.current)) {
isChangedRef.current = true;
return effect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ value ]);
};
export default useUpdateValueEffect;
...@@ -45,3 +45,7 @@ export interface SearchResult { ...@@ -45,3 +45,7 @@ export interface SearchResult {
'tx_hash': string | null; 'tx_hash': string | null;
} | null; } | null;
} }
export interface SearchResultFilters {
q: string;
}
...@@ -2,11 +2,14 @@ import { Box, chakra, Table, Tbody, Tr, Th, Skeleton } from '@chakra-ui/react'; ...@@ -2,11 +2,14 @@ import { Box, chakra, Table, Tbody, Tr, Th, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem'; import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
...@@ -58,13 +61,17 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -58,13 +61,17 @@ import { default as Thead } from 'ui/shared/TheadSticky';
const SearchResultsPageContent = () => { const SearchResultsPageContent = () => {
const router = useRouter(); const router = useRouter();
const searchTerm = String(router.query.q || ''); const searchTerm = String(router.query.q || '');
const { data, isError, isLoading } = useApiQuery('search', { const { data, isError, isLoading, pagination, isPaginationVisible, onFilterChange } = useQueryWithPages({
queryParams: { q: searchTerm }, resourceName: 'search',
queryOptions: { filters: { q: searchTerm },
enabled: Boolean(searchTerm), options: { enabled: Boolean(searchTerm) },
},
}); });
useUpdateValueEffect(() => {
onFilterChange({ q: searchTerm });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, searchTerm);
const content = (() => { const content = (() => {
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -79,17 +86,27 @@ const SearchResultsPageContent = () => { ...@@ -79,17 +86,27 @@ const SearchResultsPageContent = () => {
); );
} }
const num = pagination.page > 1 ? 50 : data.items.length;
const text = (
<Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span>
<chakra.span fontWeight={ 700 }>{ num }{ data.next_page_params || pagination.page > 1 ? '+' : '' }</chakra.span>
<span> matching results for </span>
<chakra.span fontWeight={ 700 }>{ searchTerm }</chakra.span>
</Box>
);
return ( return (
<> <>
<Box mb={ 6 }> { isPaginationVisible ? (
<span>Found </span> <ActionBar mt={ -6 }>
<chakra.span fontWeight={ 700 }>{ data.items.length }</chakra.span> { text }
<span> matching results for </span> <Pagination { ...pagination }/>
<chakra.span fontWeight={ 700 }>{ searchTerm }</chakra.span> </ActionBar>
</Box> ) : text }
{ data.items.length > 0 && ( { data.items.length > 0 && (
<Table variant="simple" size="md" fontWeight={ 500 }> <Table variant="simple" size="md" fontWeight={ 500 }>
<Thead top={ 0 }> <Thead top={ isPaginationVisible ? 80 : 0 }>
<Tr> <Tr>
<Th width="33%">Search Result</Th> <Th width="33%">Search Result</Th>
<Th width="34%">Hash/address</Th> <Th width="34%">Hash/address</Th>
......
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React 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 { getResourceKey } from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import link from 'lib/link/link'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
// const data = [ // const data = [
// { // {
...@@ -60,40 +55,24 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -60,40 +55,24 @@ export default function useSearchQuery(isSearchPage = false) {
const initialValue = isSearchPage ? String(router.query.q || '') : ''; const initialValue = isSearchPage ? String(router.query.q || '') : '';
const [ searchTerm, setSearchTerm ] = React.useState(initialValue); const [ searchTerm, setSearchTerm ] = React.useState(initialValue);
const abortControllerRef = React.useRef<AbortController>();
const apiFetch = useApiFetch();
const debouncedSearchTerm = useDebounce(searchTerm, 300); const debouncedSearchTerm = useDebounce(searchTerm, 300);
const query = useQuery<unknown, ResourceError, SearchResult>( const query = useQueryWithPages({
getResourceKey('search', { queryParams: { q: debouncedSearchTerm } }), resourceName: 'search',
() => { filters: { q: debouncedSearchTerm },
if (abortControllerRef.current) { options: { enabled: debouncedSearchTerm.trim().length > 0 },
abortControllerRef.current.abort(); });
}
abortControllerRef.current = new AbortController();
return apiFetch<'search', SearchResult>('search', {
queryParams: { q: debouncedSearchTerm },
fetchParams: { signal: abortControllerRef.current.signal },
});
},
{
enabled: debouncedSearchTerm.trim().length > 0,
},
);
const handleSearchTermChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleSearchTermChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value); setSearchTerm(event.target.value);
}, []); }, []);
React.useEffect(() => { useUpdateValueEffect(() => {
const url = link('search_results', undefined, debouncedSearchTerm ? { q: debouncedSearchTerm } : undefined); query.onFilterChange({ q: debouncedSearchTerm });
router.push(url, undefined, { shallow: true });
// should run only when debouncedSearchTerm updates // should run only when debouncedSearchTerm updates
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ debouncedSearchTerm ]); }, debouncedSearchTerm);
return { return {
searchTerm, searchTerm,
......
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