Commit 5f0e0c0e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #302 from blockscout/blocks-pagination

blocks pagination
parents 935a7414 c65ef13f
...@@ -4,23 +4,32 @@ import { useRouter } from 'next/router'; ...@@ -4,23 +4,32 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { TransactionsResponse } from 'types/api/transaction'; import type { BlockFilters } from 'types/api/block';
import type { PaginationParams } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS = [ 'block_number', 'index', 'items_count' ]; const PAGINATION_FIELDS: Array<keyof PaginationParams> = [ 'block_number', 'index', 'items_count' ];
export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, filters?: TTxsFilters) { interface ResponseWithPagination {
next_page_params: PaginationParams | null;
}
export default function useQueryWithPages<Response extends ResponseWithPagination>(
apiPath: string,
queryName: QueryKeys,
filters?: TTxsFilters | BlockFilters,
) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState(1);
const currPageParams = pick(router.query, PAGINATION_FIELDS); const currPageParams = pick(router.query, PAGINATION_FIELDS);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<TransactionsResponse['next_page_params']>>>([ {} ]); const [ pageParams, setPageParams ] = React.useState<Array<Partial<PaginationParams>>>([ {} ]);
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>( const queryResult = useQuery<unknown, unknown, Response>(
[ queryName, { page, filters } ], [ queryName, { page, filters } ],
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
...@@ -37,6 +46,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, ...@@ -37,6 +46,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
}, },
{ staleTime: Infinity }, { staleTime: Infinity },
); );
const { data } = queryResult;
const onNextPageClick = useCallback(() => { const onNextPageClick = useCallback(() => {
if (!data?.next_page_params) { if (!data?.next_page_params) {
...@@ -63,7 +73,6 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, ...@@ -63,7 +73,6 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query; let nextPageQuery: typeof router.query;
if (page === 2) { if (page === 2) {
queryClient.clear();
nextPageQuery = omit(router.query, PAGINATION_FIELDS); nextPageQuery = omit(router.query, PAGINATION_FIELDS);
} else { } else {
const nextPageParams = { ...pageParams[page - 2] }; const nextPageParams = { ...pageParams[page - 2] };
...@@ -75,6 +84,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, ...@@ -75,6 +84,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.clear();
}); });
}, [ router, page, pageParams, queryClient ]); }, [ router, page, pageParams, queryClient ]);
...@@ -88,6 +98,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, ...@@ -88,6 +98,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
}, [ router, queryClient ]); }, [ router, queryClient ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0; const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params;
const pagination = { const pagination = {
page, page,
...@@ -95,7 +106,8 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, ...@@ -95,7 +106,8 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
onPrevPageClick, onPrevPageClick,
hasPaginationParams, hasPaginationParams,
resetPage, resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
}; };
return { data, isError, isLoading, pagination }; return { ...queryResult, pagination };
} }
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler'; import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => { const getUrl = (req: NextApiRequest) => {
return `/v2/blocks${ req.query.type ? `?type=${ req.query.type }` : '' }`; const searchParamsStr = getSearchParams(req);
return `/v2/blocks${ searchParamsStr ? '?' + searchParamsStr : '' }`;
}; };
const requestHandler = handler(getUrl, [ 'GET' ]); const requestHandler = handler(getUrl, [ 'GET' ]);
......
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { PaginationParams } from 'types/api/pagination';
import type { Reward } from 'types/api/reward'; import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -33,22 +34,19 @@ export interface Block { ...@@ -33,22 +34,19 @@ export interface Block {
export interface BlocksResponse { export interface BlocksResponse {
items: Array<Block>; items: Array<Block>;
next_page_params: { next_page_params: PaginationParams | null;
block_number: number;
items_count: number;
} | null;
} }
export interface BlockTransactionsResponse { export interface BlockTransactionsResponse {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: { next_page_params: PaginationParams | null;
block_number: number;
index: number;
items_count: number;
} | null;
} }
export interface NewBlockSocketResponse { export interface NewBlockSocketResponse {
average_block_time: string; average_block_time: string;
block: Block; block: Block;
} }
export interface BlockFilters {
type?: BlockType;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { PaginationParams } from './pagination';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward' export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
...@@ -19,10 +20,7 @@ export interface InternalTransaction { ...@@ -19,10 +20,7 @@ export interface InternalTransaction {
export interface InternalTransactionsResponse { export interface InternalTransactionsResponse {
items: Array<InternalTransaction>; items: Array<InternalTransaction>;
next_page_params: { next_page_params: PaginationParams & {
block_number: number;
index: number;
items_count: number;
transaction_hash: string; transaction_hash: string;
transaction_index: number; transaction_index: number;
}; };
......
export interface PaginationParams {
block_number: number;
index?: number;
items_count: number;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { PaginationParams } from './pagination';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = { export type TransactionRevertReason = {
...@@ -45,11 +46,7 @@ export interface Transaction { ...@@ -45,11 +46,7 @@ export interface Transaction {
export interface TransactionsResponse { export interface TransactionsResponse {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: { next_page_params: PaginationParams | null;
block_number: number;
index: number;
items_count: number;
} | null;
} }
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer' export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
export enum QueryKeys { export enum QueryKeys {
addressTags = 'address-tags', addressTags = 'address-tags',
apiKeys = 'api-keys', apiKeys = 'api-keys',
block = 'block',
blocks = 'blocks',
customAbis = 'custom-abis', customAbis = 'custom-abis',
profile = 'profile', profile = 'profile',
publicTags = 'public-tags', publicTags = 'public-tags',
......
...@@ -7,4 +7,6 @@ export enum QueryKeys { ...@@ -7,4 +7,6 @@ export enum QueryKeys {
txLog = 'tx-log', txLog = 'tx-log',
txRawTrace = 'tx-raw-trace', txRawTrace = 'tx-raw-trace',
blockTxs = 'block-transactions', blockTxs = 'block-transactions',
block = 'block',
blocks = 'blocks',
} }
...@@ -7,7 +7,7 @@ import React from 'react'; ...@@ -7,7 +7,7 @@ import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
......
import { Box, Text, Show, Hide, Skeleton, Alert } from '@chakra-ui/react'; import { Text, Show, Hide, Skeleton, Alert } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile'; import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable'; import BlocksTable from 'ui/blocks/BlocksTable';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
...@@ -21,17 +22,13 @@ interface Props { ...@@ -21,17 +22,13 @@ interface Props {
} }
const BlocksContent = ({ type }: Props) => { const BlocksContent = ({ type }: Props) => {
const fetch = useFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>( const { data, isLoading, isError, pagination } = useQueryWithPages<BlocksResponse>('/node-api/blocks', QueryKeys.blocks, { type });
[ QueryKeys.blocks, type ],
async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, type ], (prevData: BlocksResponse | undefined) => { queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
const shouldAddToList = !type || type === payload.block.type; const shouldAddToList = !type || type === payload.block.type;
if (!prevData) { if (!prevData) {
...@@ -42,7 +39,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -42,7 +39,7 @@ const BlocksContent = ({ type }: Props) => {
} }
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData; return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
}); });
}, [ queryClient, type ]); }, [ pagination.page, queryClient, type ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.'); setSocketAlert('Connection is lost. Please click here to load new blocks.');
...@@ -56,7 +53,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -56,7 +53,7 @@ const BlocksContent = ({ type }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isLoading || isError, isDisabled: isLoading || isError || pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -64,38 +61,48 @@ const BlocksContent = ({ type }: Props) => { ...@@ -64,38 +61,48 @@ const BlocksContent = ({ type }: Props) => {
handler: handleNewBlockMessage, handler: handleNewBlockMessage,
}); });
if (isLoading) { const content = (() => {
if (isLoading) {
return (
<>
<Show below="lg" key="skeleton-mobile">
<BlocksSkeletonMobile/>
</Show>
<Hide below="lg" key="skeleton-desktop">
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Hide>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Text as="span">There are no blocks.</Text>;
}
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile"> { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<BlocksSkeletonMobile/> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
</Show> <Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide>
<Hide below="lg" key="skeleton-desktop">
<Skeleton h={ 6 } mb={ 8 } w="150px"/>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Hide>
</> </>
); );
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) { })();
return <Text as="span">There are no blocks.</Text>;
}
return ( return (
<> <>
<Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text> { data ?
{ socketAlert && <Alert status="warning" mt={ 8 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } <Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide> }
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <ActionBar>
{ /* eslint-disable-next-line react/jsx-no-bind */ } <Pagination ml="auto" { ...pagination }/>
<Pagination page={ 1 } onNextPageClick={ () => {} } onPrevPageClick={ () => {} } resetPage={ () => {} } hasNextPage/> </ActionBar>
</Box> { content }
</> </>
); );
}; };
......
...@@ -12,7 +12,7 @@ interface Props { ...@@ -12,7 +12,7 @@ interface Props {
const BlocksList = ({ data }: Props) => { const BlocksList = ({ data }: Props) => {
return ( return (
<Box mt={ 8 }> <Box>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ } { /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) } { data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) }
......
...@@ -17,8 +17,8 @@ interface Props { ...@@ -17,8 +17,8 @@ interface Props {
const BlocksTable = ({ data }: Props) => { const BlocksTable = ({ data }: Props) => {
return ( return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 } mt={ 8 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead top={ 0 }> <Thead top={ 80 }>
<Tr> <Tr>
<Th width="125px">Block</Th> <Th width="125px">Block</Th>
<Th width="120px">Size</Th> <Th width="120px">Size</Th>
......
...@@ -24,9 +24,7 @@ const BlockPageContent = () => { ...@@ -24,9 +24,7 @@ const BlockPageContent = () => {
return ( return (
<Page> <Page>
<PageTitle text={ `Block #${ router.query.id }` }/> <PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
tabs={ TABS }
/>
</Page> </Page>
); );
}; };
......
...@@ -17,9 +17,7 @@ const BlocksPageContent = () => { ...@@ -17,9 +17,7 @@ const BlocksPageContent = () => {
return ( return (
<Page> <Page>
<PageTitle text="Blocks"/> <PageTitle text="Blocks"/>
<RoutedTabs <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
tabs={ TABS }
/>
</Page> </Page>
); );
}; };
......
...@@ -17,7 +17,7 @@ const PrivateTags = () => { ...@@ -17,7 +17,7 @@ const PrivateTags = () => {
return ( return (
<Page> <Page>
<PageTitle text="Private tags"/> <PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page> </Page>
); );
}; };
......
...@@ -80,9 +80,7 @@ const TransactionPageContent = () => { ...@@ -80,9 +80,7 @@ const TransactionPageContent = () => {
</Flex> </Flex>
) } ) }
</Flex> </Flex>
<RoutedTabs <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
tabs={ TABS }
/>
</Page> </Page>
); );
}; };
......
...@@ -22,7 +22,7 @@ const Transactions = () => { ...@@ -22,7 +22,7 @@ const Transactions = () => {
<Page hideMobileHeaderOnScrollDown> <Page hideMobileHeaderOnScrollDown>
<Box h="100%"> <Box h="100%">
<PageTitle text="Transactions"/> <PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Box> </Box>
</Page> </Page>
); );
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import throttle from 'lodash/throttle';
import React from 'react';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
type Props = {
children: React.ReactNode;
className?: string;
}
const TOP_UP = 106;
const TOP_DOWN = 0;
const ActionBar = ({ children, className }: Props) => {
const [ isSticky, setIsSticky ] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const handleScroll = React.useCallback(() => {
if (
Number(ref.current?.getBoundingClientRect().y) < TOP_UP + 5
) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const bgColor = useColorModeValue('white', 'black');
return (
<ScrollDirectionContext.Consumer>
{ (scrollDirection) => (
<Flex
className={ className }
backgroundColor={ bgColor }
py={ 6 }
mx={{ base: -4, lg: 0 }}
px={{ base: 4, lg: 0 }}
justifyContent="space-between"
width={{ base: '100vw', lg: 'unset' }}
position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow"
zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref }
>
{ children }
</Flex>
) }
</ScrollDirectionContext.Consumer>
);
};
export default chakra(ActionBar);
import { Button, Flex, Icon, IconButton } from '@chakra-ui/react'; import { Button, Flex, Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
...@@ -10,12 +10,14 @@ export type Props = { ...@@ -10,12 +10,14 @@ export type Props = {
resetPage: () => void; resetPage: () => void;
hasNextPage: boolean; hasNextPage: boolean;
hasPaginationParams?: boolean; hasPaginationParams?: boolean;
className?: string;
} }
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams }: Props) => { const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams, className }: Props) => {
return ( return (
<Flex <Flex
className={ className }
fontSize="sm" fontSize="sm"
alignItems="center" alignItems="center"
> >
...@@ -69,4 +71,4 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -69,4 +71,4 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
); );
}; };
export default Pagination; export default chakra(Pagination);
import type { ChakraProps } from '@chakra-ui/react';
import { import {
Tab, Tab,
Tabs, Tabs,
...@@ -25,9 +26,10 @@ const hiddenItemStyles: StyleProps = { ...@@ -25,9 +26,10 @@ const hiddenItemStyles: StyleProps = {
interface Props { interface Props {
tabs: Array<RoutedTab>; tabs: Array<RoutedTab>;
tabListMarginBottom?: ChakraProps['marginBottom'];
} }
const RoutedTabs = ({ tabs }: Props) => { const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
const router = useRouter(); const router = useRouter();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => { useEffect(() => {
...@@ -59,7 +61,7 @@ const RoutedTabs = ({ tabs }: Props) => { ...@@ -59,7 +61,7 @@ const RoutedTabs = ({ tabs }: Props) => {
return ( return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }> <Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }>
<TabList <TabList
marginBottom={{ base: 6, lg: 12 }} marginBottom={ tabListMarginBottom }
flexWrap="nowrap" flexWrap="nowrap"
whiteSpace="nowrap" whiteSpace="nowrap"
ref={ listRef } ref={ listRef }
......
import { Text, Box, Show, Hide } from '@chakra-ui/react'; import { Text, Box, Show, Hide } from '@chakra-ui/react';
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsHeader from './TxsHeader'; import TxsHeader from './TxsHeader';
import TxsSkeletonDesktop from './TxsSkeletonDesktop'; import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile'; import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort'; import TxsWithSort from './TxsWithSort';
import useQueryWithPages from './useQueryWithPages';
type Props = { type Props = {
queryName: QueryKeys; queryName: QueryKeys;
...@@ -61,39 +62,36 @@ const TxsContent = ({ ...@@ -61,39 +62,36 @@ const TxsContent = ({
isLoading, isLoading,
isError, isError,
pagination, pagination,
} = useQueryWithPages(apiPath, queryName, stateFilter && { filter: stateFilter }); } = useQueryWithPages<TransactionsResponse>(apiPath, queryName, stateFilter && { filter: stateFilter });
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath }); // } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
if (isError) { const content = (() => {
return <DataFetchAlert/>; if (isError) {
} return <DataFetchAlert/>;
}
const txs = data?.items; const txs = data?.items;
if (!isLoading && !txs) { if (!isLoading && !txs?.length) {
return <Text as="span">There are no transactions.</Text>; return <Text as="span">There are no transactions.</Text>;
} }
let content = ( if (!isLoading && txs) {
<> return <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
<Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show> }
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop/></Hide>
</>
);
if (!isLoading && txs) {
content = <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
}
const paginationProps = { return (
...pagination, <>
hasNextPage: data?.next_page_params !== undefined && data?.next_page_params !== null && Object.keys(data?.next_page_params).length > 0, <Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show>
}; <Hide below="lg" ssr={ false }><TxsSkeletonDesktop/></Hide>
</>
);
})();
return ( return (
<> <>
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> } { showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> }
<TxsHeader sorting={ sorting } setSorting={ setSorting } paginationProps={ paginationProps }/> <TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSorting } paginationProps={ pagination }/>
{ content } { content }
</> </>
); );
......
import { HStack, Flex, useColorModeValue } from '@chakra-ui/react'; import { HStack, chakra } from '@chakra-ui/react';
import throttle from 'lodash/throttle'; import React from 'react';
import React, { useCallback } from 'react';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
// import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/FilterInput';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TxsSorting from 'ui/txs/TxsSorting'; import TxsSorting from 'ui/txs/TxsSorting';
...@@ -17,89 +16,40 @@ type Props = { ...@@ -17,89 +16,40 @@ type Props = {
sorting: Sort; sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void; setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
paginationProps: PaginationProps; paginationProps: PaginationProps;
className?: string;
} }
const TOP_UP = 106; const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) => {
const TOP_DOWN = 0;
const TxsHeader = ({ sorting, setSorting, paginationProps }: Props) => {
const [ isSticky, setIsSticky ] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(false); const isMobile = useIsMobile(false);
const handleScroll = useCallback(() => {
if (
Number(ref.current?.getBoundingClientRect().y) < TOP_UP + 5
) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const bgColor = useColorModeValue('white', 'black');
return ( return (
<ScrollDirectionContext.Consumer> <ActionBar className={ className }>
{ (scrollDirection) => ( <HStack>
<Flex { /* api is not implemented */ }
backgroundColor={ bgColor } { /* <TxsFilters
mt={ -6 }
pt={ 6 }
pb={ 6 }
mx={{ base: -4, lg: 0 }}
px={{ base: 4, lg: 0 }}
justifyContent="space-between"
width={{ base: '100vw', lg: 'unset' }}
position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow"
transitionDuration="slow"
zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref }
>
<HStack>
{ /* api is not implemented */ }
{ /* <TxsFilters
filters={ filters } filters={ filters }
onFiltersChange={ setFilters } onFiltersChange={ setFilters }
appliedFiltersNum={ 0 } appliedFiltersNum={ 0 }
/> */ } /> */ }
{ isMobile && ( { isMobile && (
<TxsSorting <TxsSorting
isActive={ Boolean(sorting) } isActive={ Boolean(sorting) }
setSorting={ setSorting } setSorting={ setSorting }
sorting={ sorting } sorting={ sorting }
/> />
) } ) }
{ /* api is not implemented */ } { /* api is not implemented */ }
{ /* <FilterInput { /* <FilterInput
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} } onChange={ () => {} }
maxW="360px" maxW="360px"
size="xs" size="xs"
placeholder="Search by addresses, hash, method..." placeholder="Search by addresses, hash, method..."
/> */ } /> */ }
</HStack> </HStack>
<Pagination { ...paginationProps }/> <Pagination { ...paginationProps }/>
</Flex> </ActionBar>
) }
</ScrollDirectionContext.Consumer>
); );
}; };
export default TxsHeader; export default chakra(TxsHeader);
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