Commit 3bd5b6cd authored by tom's avatar tom

pagination

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