mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
sticky pagination
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import clamp from 'lodash/clamp';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React from 'react';
|
||||
|
||||
import isBrowser from 'lib/isBrowser';
|
||||
|
||||
const SCROLL_DIFF_THRESHOLD = 20;
|
||||
|
||||
export default function useScrollVisibility(direction: 'up' | 'down') {
|
||||
const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0);
|
||||
const [ isVisible, setVisibility ] = React.useState(true);
|
||||
|
||||
const handleScroll = React.useCallback(() => {
|
||||
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
|
||||
const scrollDiff = currentScrollPosition - prevScrollPosition.current;
|
||||
|
||||
if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
|
||||
setVisibility(direction === 'up' ? scrollDiff < 0 : scrollDiff > 0);
|
||||
}
|
||||
|
||||
prevScrollPosition.current = currentScrollPosition;
|
||||
}, [ direction ]);
|
||||
|
||||
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
|
||||
}, []);
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const BlocksContent = ({ type }: Props) => {
|
||||
<Show above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
|
||||
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
|
||||
{ /* eslint-disable-next-line react/jsx-no-bind */ }
|
||||
<Pagination currentPage={ 1 } onNextPageClick={ () => {} } onPrevPageClick={ () => {} } hasNextPage/>
|
||||
<Pagination page={ 1 } onNextPageClick={ () => {} } onPrevPageClick={ () => {} } resetPage={ () => {} } hasNextPage/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
+46
-60
@@ -3,77 +3,63 @@ import React from 'react';
|
||||
|
||||
import arrowIcon from 'icons/arrows/east-mini.svg';
|
||||
|
||||
type Props = {
|
||||
currentPage: number;
|
||||
maxPage?: number;
|
||||
export type Props = {
|
||||
page: number;
|
||||
onNextPageClick: () => void;
|
||||
onPrevPageClick: () => void;
|
||||
resetPage: () => void;
|
||||
hasNextPage: boolean;
|
||||
hasPaginationParams?: boolean;
|
||||
}
|
||||
|
||||
const MAX_PAGE_DEFAULT = 50;
|
||||
|
||||
const Pagination = ({ currentPage, maxPage, onNextPageClick, onPrevPageClick, hasNextPage }: Props) => {
|
||||
const pageNumber = (
|
||||
<Flex alignItems="center">
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
isActive
|
||||
borderWidth="1px"
|
||||
fontWeight={ 400 }
|
||||
mr={ 3 }
|
||||
h={ 8 }
|
||||
>
|
||||
{ currentPage }
|
||||
</Button>
|
||||
{ /* max page will be removed */ }
|
||||
of
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
width={ 8 }
|
||||
borderWidth="1px"
|
||||
fontWeight={ 400 }
|
||||
ml={ 3 }
|
||||
>
|
||||
{ maxPage || MAX_PAGE_DEFAULT }
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams }: Props) => {
|
||||
|
||||
return (
|
||||
<Flex
|
||||
fontSize="sm"
|
||||
width={{ base: '100%', lg: 'auto' }}
|
||||
justifyContent={{ base: 'space-between', lg: 'unset' }}
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between" w={{ base: '100%', lg: 'auto' }}>
|
||||
<IconButton
|
||||
variant="outline"
|
||||
onClick={ onPrevPageClick }
|
||||
size="sm"
|
||||
aria-label="Next page"
|
||||
w="36px"
|
||||
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
|
||||
mr={ 8 }
|
||||
disabled={ currentPage === 1 }
|
||||
/>
|
||||
{ pageNumber }
|
||||
<IconButton
|
||||
variant="outline"
|
||||
onClick={ onNextPageClick }
|
||||
size="sm"
|
||||
aria-label="Next page"
|
||||
w="36px"
|
||||
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
|
||||
ml={ 8 }
|
||||
disabled={ !hasNextPage }
|
||||
/>
|
||||
</Flex>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={ resetPage }
|
||||
disabled={ !hasPaginationParams }
|
||||
mr={ 4 }
|
||||
>
|
||||
First
|
||||
</Button>
|
||||
<IconButton
|
||||
variant="outline"
|
||||
onClick={ onPrevPageClick }
|
||||
size="sm"
|
||||
aria-label="Next page"
|
||||
w="36px"
|
||||
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
|
||||
mr={ 6 }
|
||||
disabled={ page === 1 }
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isActive
|
||||
borderWidth="1px"
|
||||
fontWeight={ 400 }
|
||||
h={ 8 }
|
||||
cursor="unset"
|
||||
disabled={ hasPaginationParams && page === 1 }
|
||||
>
|
||||
{ page }
|
||||
</Button>
|
||||
<IconButton
|
||||
variant="outline"
|
||||
onClick={ onNextPageClick }
|
||||
size="sm"
|
||||
aria-label="Next page"
|
||||
w="36px"
|
||||
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
|
||||
ml={ 6 }
|
||||
disabled={ !hasNextPage }
|
||||
/>
|
||||
{ /* not implemented yet */ }
|
||||
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
|
||||
Go to <Input w="84px" size="xs" ml={ 2 }/>
|
||||
|
||||
@@ -20,6 +20,7 @@ const SortButton = ({ onClick, isActive, className }: Props) => {
|
||||
minWidth="36px"
|
||||
onClick={ onClick }
|
||||
isActive={ isActive }
|
||||
display="flex"
|
||||
className={ className }
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { InputGroup, Input, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
|
||||
import clamp from 'lodash/clamp';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React from 'react';
|
||||
import type { ChangeEvent, FormEvent } from 'react';
|
||||
|
||||
import searchIcon from 'icons/search.svg';
|
||||
import isBrowser from 'lib/isBrowser';
|
||||
|
||||
const SCROLL_DIFF_THRESHOLD = 20;
|
||||
import useScrollVisibility from 'lib/hooks/useScrollVisibility';
|
||||
|
||||
interface Props {
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
@@ -16,36 +12,12 @@ interface Props {
|
||||
|
||||
const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
|
||||
|
||||
const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0);
|
||||
const [ isVisible, setVisibility ] = React.useState(true);
|
||||
const isVisible = useScrollVisibility('up');
|
||||
|
||||
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
|
||||
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
|
||||
const bgColor = useColorModeValue('white', 'black');
|
||||
|
||||
const handleScroll = React.useCallback(() => {
|
||||
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
|
||||
const scrollDiff = currentScrollPosition - prevScrollPosition.current;
|
||||
|
||||
if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
|
||||
setVisibility(scrollDiff > 0 ? false : true);
|
||||
}
|
||||
|
||||
prevScrollPosition.current = currentScrollPosition;
|
||||
}, []);
|
||||
|
||||
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
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<chakra.form
|
||||
noValidate
|
||||
|
||||
+9
-46
@@ -1,4 +1,4 @@
|
||||
import { Alert, Box, HStack, Show, Button } from '@chakra-ui/react';
|
||||
import { Alert, Box, Show } from '@chakra-ui/react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import type { TTxsFilters } from 'types/api/txsFilters';
|
||||
@@ -11,7 +11,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
// import FilterInput from 'ui/shared/FilterInput';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
|
||||
// import TxsFilters from './TxsFilters';
|
||||
import TxsHeader from './TxsHeader';
|
||||
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
|
||||
import TxsSkeletonMobile from './TxsSkeletonMobile';
|
||||
import TxsSorting from './TxsSorting';
|
||||
@@ -64,16 +64,10 @@ const TxsContent = ({
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
page,
|
||||
onPrevPageClick,
|
||||
onNextPageClick,
|
||||
hasPagination,
|
||||
resetPage,
|
||||
pagination,
|
||||
} = useQueryWithPages(apiPath, queryName, stateFilter && { filter: stateFilter });
|
||||
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
|
||||
|
||||
const isMobile = useIsMobile(false);
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
@@ -95,47 +89,16 @@ const TxsContent = ({
|
||||
content = <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
|
||||
}
|
||||
|
||||
const paginationProps = {
|
||||
...pagination,
|
||||
hasNextPage: data?.next_page_params !== undefined && Object.keys(data?.next_page_params).length > 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
|
||||
<HStack mb={ 6 }>
|
||||
{ /* api is not implemented */ }
|
||||
{ /* <TxsFilters
|
||||
filters={ filters }
|
||||
onFiltersChange={ setFilters }
|
||||
appliedFiltersNum={ 0 }
|
||||
/> */ }
|
||||
{ isMobile && (
|
||||
<TxsSorting
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
isActive={ Boolean(sorting) }
|
||||
setSorting={ setSorting }
|
||||
sorting={ sorting }
|
||||
/>
|
||||
) }
|
||||
{ /* api is not implemented */ }
|
||||
{ /* <FilterInput
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange={ () => {} }
|
||||
maxW="360px"
|
||||
size="xs"
|
||||
placeholder="Search by addresses, hash, method..."
|
||||
/> */ }
|
||||
</HStack>
|
||||
<TxsHeader sorting={ sorting } paginationProps={ paginationProps }/>
|
||||
{ content }
|
||||
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
|
||||
{ hasPagination ? (
|
||||
<Pagination
|
||||
currentPage={ page }
|
||||
hasNextPage={ data?.next_page_params !== undefined && Object.keys(data?.next_page_params || {}).length > 0 }
|
||||
onNextPageClick={ onNextPageClick }
|
||||
onPrevPageClick={ onPrevPageClick }
|
||||
/>
|
||||
) :
|
||||
// temporary button, waiting for new pagination mockups
|
||||
<Button onClick={ resetPage }>Reset</Button>
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { HStack, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Sort } from 'types/client/txs-sort';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
// import FilterInput from 'ui/shared/FilterInput';
|
||||
import useScrollVisibility from 'lib/hooks/useScrollVisibility';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import SortButton from 'ui/shared/SortButton';
|
||||
|
||||
// import TxsFilters from './TxsFilters';
|
||||
|
||||
type Props = {
|
||||
sorting: Sort;
|
||||
paginationProps: PaginationProps;
|
||||
}
|
||||
|
||||
const TxsHeader = ({ sorting, paginationProps }: Props) => {
|
||||
const isVisible = useScrollVisibility('down');
|
||||
|
||||
const isMobile = useIsMobile(false);
|
||||
return (
|
||||
<Flex
|
||||
backgroundColor="white"
|
||||
mt={ -6 }
|
||||
pt={ 6 }
|
||||
pb={ 6 }
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
position="sticky"
|
||||
top={{ base: isVisible ? '56px' : '-56px', lg: 0 }}
|
||||
transitionDuration="slow"
|
||||
transitionProperty="top"
|
||||
zIndex={{ base: 0, lg: 'docked' }}
|
||||
>
|
||||
<HStack>
|
||||
{ /* api is not implemented */ }
|
||||
{ /* <TxsFilters
|
||||
filters={ filters }
|
||||
onFiltersChange={ setFilters }
|
||||
appliedFiltersNum={ 0 }
|
||||
/> */ }
|
||||
{ isMobile && (
|
||||
<SortButton
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
handleSort={ () => {} }
|
||||
isSortActive={ Boolean(sorting) }
|
||||
/>
|
||||
) }
|
||||
{ /* api is not implemented */ }
|
||||
{ /* <FilterInput
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange={ () => {} }
|
||||
maxW="360px"
|
||||
size="xs"
|
||||
placeholder="Search by addresses, hash, method..."
|
||||
/> */ }
|
||||
</HStack>
|
||||
<Pagination { ...paginationProps }/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxsHeader;
|
||||
+1
-1
@@ -17,7 +17,7 @@ type Props = {
|
||||
|
||||
const TxsTable = ({ txs, sort, sorting }: Props) => {
|
||||
return (
|
||||
<TableContainer width="100%" mt={ 6 }>
|
||||
<TableContainer width="100%">
|
||||
<Table variant="simple" minWidth="810px" size="xs">
|
||||
<Thead>
|
||||
<Tr>
|
||||
|
||||
@@ -63,6 +63,7 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
|
||||
// we dont have pagination params for the first page
|
||||
let nextPageQuery: typeof router.query;
|
||||
if (page === 2) {
|
||||
queryClient.clear();
|
||||
nextPageQuery = omit(router.query, PAGINATION_FIELDS);
|
||||
} else {
|
||||
const nextPageParams = { ...pageParams[page - 2] };
|
||||
@@ -75,16 +76,26 @@ export default function useQueryWithPages(apiPath: string, queryName: QueryKeys,
|
||||
animateScroll.scrollToTop({ duration: 0 });
|
||||
setPage(prev => prev - 1);
|
||||
});
|
||||
}, [ router, page, pageParams ]);
|
||||
}, [ router, page, pageParams, queryClient ]);
|
||||
|
||||
const resetPage = useCallback(() => {
|
||||
queryClient.clear();
|
||||
animateScroll.scrollToTop({ duration: 0 });
|
||||
router.push({ pathname: router.pathname, query: omit(router.query, PAGINATION_FIELDS) }, undefined, { shallow: true });
|
||||
router.push({ pathname: router.pathname, query: omit(router.query, PAGINATION_FIELDS) }, undefined, { shallow: true }).then(() => {
|
||||
animateScroll.scrollToTop({ duration: 0 });
|
||||
setPage(1);
|
||||
setPageParams([ {} ]);
|
||||
});
|
||||
}, [ router, queryClient ]);
|
||||
|
||||
// if there are pagination params on the initial page, we shouldn't show pagination
|
||||
const hasPagination = !(page === 1 && Object.keys(currPageParams).length > 0);
|
||||
const hasPaginationParams = Object.keys(currPageParams).length > 0;
|
||||
|
||||
return { data, isError, isLoading, page, onNextPageClick, onPrevPageClick, hasPagination, resetPage };
|
||||
const pagination = {
|
||||
page,
|
||||
onNextPageClick,
|
||||
onPrevPageClick,
|
||||
hasPaginationParams,
|
||||
resetPage,
|
||||
};
|
||||
|
||||
return { data, isError, isLoading, pagination };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user