Commit dba0d25a authored by isstuev's avatar isstuev

scroll and sorting preserve

parent b1219d40
import React from 'react'; import React from 'react';
import TxsContent from 'ui/txs/TxsContent'; import TxsWithSort from 'ui/txs/TxsWithSort';
const BlockTxs = () => { const BlockTxs = () => {
return ( return (
<TxsContent // <TxsContent
showDescription={ false } // showDescription={ false }
showSortButton={ false } // showSortButton={ false }
txs={ [] } // txs={ [] }
page={ 1 } // page={ 1 }
// eslint-disable-next-line react/jsx-no-bind // // eslint-disable-next-line react/jsx-no-bind
onNextPageClick={ () => {} } // onNextPageClick={ () => {} }
// eslint-disable-next-line react/jsx-no-bind // // eslint-disable-next-line react/jsx-no-bind
onPrevPageClick={ () => {} } // onPrevPageClick={ () => {} }
/> // />
// eslint-disable-next-line react/jsx-no-bind
<TxsWithSort txs={ [] } setSorting={ () => {} }/>
); );
}; };
......
...@@ -9,15 +9,13 @@ import appConfig from 'configs/app/config'; ...@@ -9,15 +9,13 @@ import appConfig from 'configs/app/config';
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 RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending'; import TxsTab from 'ui/txs/TxsTab';
import TxsValidated from 'ui/txs/TxsValidated';
const Transactions = () => { const Transactions = () => {
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined'; const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'validated', title: verifiedTitle, component: <TxsValidated/> }, { id: 'validated', title: verifiedTitle, component: <TxsTab tab="validated"/> },
{ id: 'pending', title: 'Pending', component: <TxsPending/> }, { id: 'pending', title: 'Pending', component: <TxsTab tab="pending"/> },
]; ];
return ( return (
......
import { Button, Flex, Input, Icon, IconButton } from '@chakra-ui/react'; import { Button, Flex, Icon, IconButton } 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';
...@@ -70,9 +70,10 @@ const Pagination = ({ currentPage, maxPage, onNextPageClick, onPrevPageClick }: ...@@ -70,9 +70,10 @@ const Pagination = ({ currentPage, maxPage, onNextPageClick, onPrevPageClick }:
ml={ 8 } ml={ 8 }
/> />
</Flex> </Flex>
<Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}> { /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/> Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex> </Flex> */ }
</Flex> </Flex>
); );
......
import { Box, HStack, Show } from '@chakra-ui/react'; import { Alert, Box, HStack, Show, Button } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns'; import useIsMobile from 'lib/hooks/useIsMobile';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton'; import SortButton from 'ui/shared/SortButton';
import TxsListItem from './TxsListItem'; import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsTable from './TxsTable'; import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort';
import useQueryWithPages from './useQueryWithPages';
type Props = { type Props = {
txs: TransactionsResponse['items']; queryName: string;
showDescription?: boolean; showDescription?: boolean;
showSortButton?: boolean; stateFilter: 'validated' | 'pending';
onNextPageClick: () => void;
onPrevPageClick: () => void;
page: number;
} }
const TxsContent = ({ showSortButton = true, showDescription = true, txs, page, onNextPageClick, onPrevPageClick }: Props) => { const TxsContent = ({
showDescription,
queryName,
stateFilter,
}: Props) => {
const [ sorting, setSorting ] = useState<Sort>(); const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
// sorting should be preserved with pagination! const {
const sort = useCallback((field: 'val' | 'fee') => () => { data,
if (field === 'val') { isLoading,
setSorting((prevVal => { isError,
if (prevVal === 'val-asc') { page,
return undefined; onPrevPageClick,
} onNextPageClick,
if (prevVal === 'val-desc') { hasPagination,
return 'val-asc'; resetPage,
} } = useQueryWithPages(queryName, stateFilter);
return 'val-desc';
})); const isMobile = useIsMobile(false);
}
if (field === 'fee') { if (isError) {
setSorting((prevVal => { return <DataFetchAlert/>;
if (prevVal === 'fee-asc') { }
return undefined;
} const txs = data?.items;
if (prevVal === 'fee-desc') {
return 'fee-asc';
}
return 'fee-desc';
}));
}
}, []);
useEffect(() => { if (!isLoading && !txs) {
switch (sorting) { return <Alert>There are no transactions.</Alert>;
case 'val-desc': }
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break; let content = (
case 'val-asc': <>
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value))); <Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show>
break; <Show above="lg" ssr={ false }><TxsSkeletonDesktop/></Show>
case 'fee-desc': </>
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value))); );
break;
case 'fee-asc': if (!isLoading && txs) {
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value))); content = <TxsWithSort txs={ txs } sorting={ sorting } setSorting={ setSorting }/>;
break; }
default:
setSortedTxs(txs);
}
}, [ sorting, txs ]);
return ( return (
<> <>
...@@ -82,7 +73,7 @@ const TxsContent = ({ showSortButton = true, showDescription = true, txs, page, ...@@ -82,7 +73,7 @@ const TxsContent = ({ showSortButton = true, showDescription = true, txs, page,
onClick={ () => {} } onClick={ () => {} }
appliedFiltersNum={ 0 } appliedFiltersNum={ 0 }
/> />
{ showSortButton && ( { isMobile && (
<SortButton <SortButton
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
handleSort={ () => {} } handleSort={ () => {} }
...@@ -98,10 +89,12 @@ const TxsContent = ({ showSortButton = true, showDescription = true, txs, page, ...@@ -98,10 +89,12 @@ const TxsContent = ({ showSortButton = true, showDescription = true, txs, page,
placeholder="Search by addresses, hash, method..." placeholder="Search by addresses, hash, method..."
/> />
</HStack> </HStack>
<Show below="lg" ssr={ false }><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show> { content }
<Show above="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ page } onNextPageClick={ onNextPageClick } onPrevPageClick={ onPrevPageClick }/> { hasPagination ?
<Pagination currentPage={ page } onNextPageClick={ onNextPageClick } onPrevPageClick={ onPrevPageClick }/> :
<Button onClick={ resetPage }>Reset</Button>
}
</Box> </Box>
</> </>
); );
......
import { Show, Alert } from '@chakra-ui/react';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import useQueryWithPages from './useQueryWithPages';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ QueryKeys.transactionsPending ], async() => fetch('/api/transactions/?filter=pending'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile isPending/></Show>
<Show above="lg"><TxsSkeletonDesktop isPending/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return (
<TxsContent
txs={ data.items }
showDescription={ false }
page={ page }
onPrevPageClick={ onPrevPageClick }
onNextPageClick={ onNextPageClick }
/>
);
};
export default TxsValidated;
import { Skeleton, Flex } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => { const TxsInternalsSkeletonDesktop = () => {
return ( return (
<> <Box mb={ 8 }>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/> <SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</> </Box>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react'; import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => { const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<> <Box>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> } { Array.from(Array(2)).map((item, index) => (
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }> <Flex
<Skeleton w="36px" flexShrink={ 0 }/> key={ index }
<Skeleton w="36px" flexShrink={ 0 }/> flexDirection="column"
<Skeleton w="100%"/> paddingBottom={ 3 }
</Flex> paddingTop={ 4 }
<Box> borderTopWidth="1px"
{ Array.from(Array(2)).map((item, index) => ( borderColor={ borderColor }
<Flex _last={{
key={ index } borderBottomWidth: '1px',
flexDirection="column" }}
paddingBottom={ 3 } >
paddingTop={ 4 } <Flex h={ 6 }>
borderTopWidth="1px" <Skeleton w="100px" mr={ 2 } h={ 6 }/>
borderColor={ borderColor } <Skeleton w="100px" h={ 6 }/>
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex> </Flex>
)) } <Skeleton w="100%" h="30px" mt={ 3 }/>
</Box> <Skeleton w="50%" h={ 6 } mt={ 3 }/>
</> <Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
); );
}; };
......
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from './TxsContent';
type Props = {
tab: 'validated' | 'pending';
}
const TxsTab = ({ tab }: Props) => {
return (
<TxsContent
queryName={ tab === 'validated' ? QueryKeys.transactionsValidated : QueryKeys.transactionsPending }
showDescription={ tab === 'validated' }
stateFilter={ tab }
/>
);
};
export default TxsTab;
...@@ -12,7 +12,6 @@ import { ...@@ -12,7 +12,6 @@ import {
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
PopoverBody, PopoverBody,
Portal,
useColorModeValue, useColorModeValue,
Show, Show,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
...@@ -57,19 +56,17 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -57,19 +56,17 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
return ( return (
<Tr> <Tr>
<Td pl={ 4 }> <Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }> <Popover placement="right-start" openDelay={ 300 } isLazy>
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/> <TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger> </PopoverTrigger>
<Portal> <PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }> <PopoverBody>
<PopoverBody> <TxAdditionalInfo tx={ tx }/>
<TxAdditionalInfo tx={ tx }/> </PopoverBody>
</PopoverBody> </PopoverContent>
</PopoverContent>
</Portal>
</> </>
) } ) }
</Popover> </Popover>
......
import { Show, Alert } from '@chakra-ui/react';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import useQueryWithPages from './useQueryWithPages';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ QueryKeys.transactionsValidated ], async() => fetch('/api/transactions/?filter=validated'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile/></Show>
<Show above="lg"><TxsSkeletonDesktop/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return <TxsContent txs={ data.items } page={ page } onNextPageClick={ onNextPageClick } onPrevPageClick={ onPrevPageClick }/>;
};
export default TxsValidated;
import { Box, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
sorting?: Sort;
setSorting: (sorting: Sort | ((val: Sort) => Sort)) => void;
}
const TxsContent = ({
txs,
sorting,
setSorting,
}: Props) => {
const [ sortedTxs, setSortedTxs ] = useState(txs);
const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') {
setSorting((prevVal => {
if (prevVal === 'val-asc') {
return undefined;
}
if (prevVal === 'val-desc') {
return 'val-asc';
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
if (prevVal === 'fee-asc') {
return undefined;
}
if (prevVal === 'fee-desc') {
return 'fee-asc';
}
return 'fee-desc';
}));
}
}, [ setSorting ]);
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)));
break;
default:
setSortedTxs(txs);
}
}, [ sorting, txs ]);
return (
<>
<Show below="lg" ssr={ false }><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
</>
);
};
export default TxsContent;
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { pick, omit } from 'lodash';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { TransactionsResponse } from 'types/api/transaction'; import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
export default function useQueryWithPages(queryName: string) { const PAGINATION_FIELDS = [ 'block_number', 'index', 'items_count' ];
export default function useQueryWithPages(queryName: string, filter: string) {
const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState(1);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<TransactionsResponse['next_page_params']>>>([ {} ]); const [ pageParams, setPageParams ] = React.useState<Array<Partial<TransactionsResponse['next_page_params']>>>([ {} ]);
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>(
[ queryName, page ], [ queryName, { page } ],
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
Object.entries(pageParams[page - 1]).forEach(([ key, val ]) => params.push(`${ key }=${ val }`)); Object.entries(pageParams[page - 1]).forEach(([ key, val ]) => params.push(`${ key }=${ val }`));
return fetch(`/api/transactions?filter=validated${ params.length ? '&' + params.join('&') : '' }`); return fetch(`/api/transactions?filter=${ filter }${ params.length ? '&' + params.join('&') : '' }`);
}, },
{ staleTime: Infinity },
); );
const onNextPageClick = useCallback(() => { const onNextPageClick = useCallback(() => {
if (page >= pageParams.length && data?.next_page_params) { if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, data?.next_page_params ]); // api adds filters into next-page-params now
// later filters will be removed from response
const nextPageParams = pick(data?.next_page_params, PAGINATION_FIELDS);
setPageParams(prev => [ ...prev, nextPageParams ]);
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
router.query = nextPageQuery;
router.push(router);
} }
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1); setPage(prev => prev + 1);
}, [ data, page, pageParams ]); }, [ data, page, pageParams, router ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning for the first page
// we dont have pagination params for the first page
if (page === 2) {
router.query = omit(router.query, PAGINATION_FIELDS);
} else {
const nextPageParams = { ...pageParams[page - 1] };
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
router.query = nextPageQuery;
}
router.push(router);
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev - 1); setPage(prev => prev - 1);
}, []); }, [ router, page, pageParams ]);
const resetPage = useCallback(() => {
router.query = omit(router.query, PAGINATION_FIELDS);
router.push(router);
animateScroll.scrollToTop({ duration: 0 });
setPageParams([ {} ]);
setPage(1);
}, [ router ]);
// if there are pagination params on the initial page, we shouldn't show pagination
const hasPagination = !(page === 1 && Object.keys(pick(router.query, PAGINATION_FIELDS)).length > 0);
return { data, isError, isLoading, page, onNextPageClick, onPrevPageClick }; return { data, isError, isLoading, page, onNextPageClick, onPrevPageClick, hasPagination, resetPage };
} }
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