Commit 9bef6056 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #799 from blockscout/watchlist-txs

Watchlist txs
parents 90a73c95 20785776
...@@ -39,7 +39,7 @@ import type { ...@@ -39,7 +39,7 @@ import type {
} from 'types/api/token'; } from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens'; import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
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, TransactionsResponseWatchlist } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
...@@ -145,6 +145,11 @@ export const RESOURCES = { ...@@ -145,6 +145,11 @@ export const RESOURCES = {
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ], paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_watchlist: {
path: '/api/v2/transactions/watchlist',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ ],
},
tx: { tx: {
path: '/api/v2/transactions/:hash', path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
...@@ -370,6 +375,9 @@ export const RESOURCES = { ...@@ -370,6 +375,9 @@ export const RESOURCES = {
homepage_txs: { homepage_txs: {
path: '/api/v2/main-page/transactions', path: '/api/v2/main-page/transactions',
}, },
homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist',
},
homepage_indexing_status: { homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status', path: '/api/v2/main-page/indexing-status',
}, },
...@@ -490,7 +498,7 @@ export interface ResourceError<T = unknown> { ...@@ -490,7 +498,7 @@ export interface ResourceError<T = unknown> {
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> 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' | 'txs_watchlist' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' | 'addresses' |
'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' |
...@@ -518,6 +526,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse : ...@@ -518,6 +526,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> : Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters : Q extends 'stats_counters' ? Counters :
...@@ -529,6 +538,7 @@ Q extends 'block_txs' ? BlockTransactionsResponse : ...@@ -529,6 +538,7 @@ Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'txs_watchlist' ? TransactionsResponseWatchlist :
Q extends 'tx' ? Transaction : Q extends 'tx' ? Transaction :
Q extends 'tx_internal_txs' ? InternalTransactionsResponse : Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
......
import appConfig from 'configs/app/config';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
export default function useHasAccount() {
const appProps = useAppContext();
if (!appConfig.isAccountSupported) {
return false;
}
const cookiesString = appProps.cookies;
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
return hasAuth;
}
...@@ -198,7 +198,7 @@ export default function useNavItems(): ReturnType { ...@@ -198,7 +198,7 @@ export default function useNavItems(): ReturnType {
const accountNavItems = [ const accountNavItems = [
{ {
text: 'Watchlist', text: 'Watch list',
nextRoute: { pathname: '/account/watchlist' as const }, nextRoute: { pathname: '/account/watchlist' as const },
icon: watchlistIcon, icon: watchlistIcon,
isActive: pathname === '/account/watchlist', isActive: pathname === '/account/watchlist',
......
...@@ -17,7 +17,9 @@ export function middleware(req: NextRequest) { ...@@ -17,7 +17,9 @@ export function middleware(req: NextRequest) {
} }
// we don't have any info from router here, so just do straight forward sub-string search (sorry) // we don't have any info from router here, so just do straight forward sub-string search (sorry)
const isAccountRoute = req.nextUrl.pathname.includes('/account/'); const isAccountRoute =
req.nextUrl.pathname.includes('/account/') ||
(req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist');
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
const apiToken = req.cookies.get(NAMES.API_TOKEN); const apiToken = req.cookies.get(NAMES.API_TOKEN);
......
...@@ -70,6 +70,15 @@ export interface TransactionsResponsePending { ...@@ -70,6 +70,15 @@ export interface TransactionsResponsePending {
} | null; } | null;
} }
export interface TransactionsResponseWatchlist {
items: Array<Transaction>;
next_page_params: {
inserted_at: string;
hash: string;
filter: 'pending';
} | 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 type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse; export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
...@@ -9,11 +9,11 @@ import React from 'react'; ...@@ -9,11 +9,11 @@ import React from 'react';
import type { Chain } from 'wagmi'; import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi'; import { configureChains, createClient, WagmiConfig } from 'wagmi';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedSubTab } from 'ui/shared/Tabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { ContractContextProvider } from 'ui/address/contract/context'; import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
......
...@@ -9,7 +9,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -9,7 +9,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address'; import { tokenTabsByType } from 'ui/pages/Address';
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 RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import ERC1155Tokens from './tokens/ERC1155Tokens'; import ERC1155Tokens from './tokens/ERC1155Tokens';
import ERC20Tokens from './tokens/ERC20Tokens'; import ERC20Tokens from './tokens/ERC20Tokens';
......
...@@ -11,6 +11,7 @@ const LatestTxsItemSkeleton = () => { ...@@ -11,6 +11,7 @@ const LatestTxsItemSkeleton = () => {
return ( return (
<Box <Box
width="100%" width="100%"
minW="700px"
borderTop="1px solid" borderTop="1px solid"
borderColor="divider" borderColor="divider"
py={ 4 } py={ 4 }
......
import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import LinkInternal from 'ui/shared/LinkInternal';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestWatchlistTxs = () => {
useRedirectForInvalidAuthToken();
const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs_watchlist');
if (isLoading) {
return <>{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }</>;
}
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data.length === 0) {
return <Text mt={ 4 }>There are no transactions.</Text>;
}
if (data) {
const txsUrl = route({ pathname: '/txs', query: { tab: 'watchlist' } });
return (
<>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal>
</Flex>
</>
);
}
return null;
};
export default LatestWatchlistTxs;
import { Heading, Tab, Tabs, TabList, TabPanel, TabPanels } from '@chakra-ui/react'; import { Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useHasAccount from 'lib/hooks/useHasAccount';
import LatestDeposits from 'ui/home/LatestDeposits'; import LatestDeposits from 'ui/home/LatestDeposits';
import LatestTxs from 'ui/home/LatestTxs'; import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
const TransactionsHome = () => { const TransactionsHome = () => {
if (appConfig.L2.isL2Network) { const hasAccount = useHasAccount();
if (appConfig.L2.isL2Network || hasAccount) {
const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
appConfig.L2.isL2Network && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean);
return ( return (
<> <>
<Heading as="h4" size="sm" mb={ 4 }>Transactions</Heading> <Heading as="h4" size="sm" mb={ 4 }>Transactions</Heading>
<Tabs isLazy lazyBehavior="keepMounted" defaultIndex={ 0 } variant="soft-rounded"> <TabsWithScroll tabs={ tabs } lazyBehavior="keepMounted"/>
<TabList>
<Tab key="txn">Latest txn</Tab>
<Tab key="deposits">Deposits (L1→L2 txn)</Tab>
</TabList>
<TabPanels mt={ 4 }>
<TabPanel key="txn" p={ 0 }>
<LatestTxs/>
</TabPanel>
<TabPanel key="deposits" p={ 0 }>
<LatestDeposits/>
</TabPanel>
</TabPanels>
</Tabs>
</> </>
); );
} }
......
...@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
...@@ -24,8 +24,8 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals'; ...@@ -24,8 +24,8 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
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 SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
export const tokenTabsByType: Record<TokenType, string> = { export const tokenTabsByType: Record<TokenType, string> = {
'ERC-20': 'tokens_erc20', 'ERC-20': 'tokens_erc20',
......
...@@ -2,7 +2,7 @@ import { Skeleton } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
...@@ -16,8 +16,8 @@ import TextAd from 'ui/shared/ad/TextAd'; ...@@ -16,8 +16,8 @@ import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
......
...@@ -2,7 +2,7 @@ import { useRouter } from 'next/router'; ...@@ -2,7 +2,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { BlockType } from 'types/api/block'; import type { BlockType } from 'types/api/block';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -10,7 +10,7 @@ import BlocksContent from 'ui/blocks/BlocksContent'; ...@@ -10,7 +10,7 @@ import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot'; import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
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/Tabs/RoutedTabs';
const TAB_TO_TYPE: Record<string, BlockType> = { const TAB_TO_TYPE: Record<string, BlockType> = {
blocks: 'block', blocks: 'block',
......
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
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/Tabs/RoutedTabs';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'address', title: 'Address', component: <PrivateAddressTags/> }, { id: 'address', title: 'Address', component: <PrivateAddressTags/> },
......
...@@ -5,7 +5,7 @@ import React, { useEffect } from 'react'; ...@@ -5,7 +5,7 @@ import React, { useEffect } from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
...@@ -24,8 +24,8 @@ import Tag from 'ui/shared/chakra/Tag'; ...@@ -24,8 +24,8 @@ import Tag from 'ui/shared/chakra/Tag';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
......
...@@ -2,7 +2,7 @@ import { Flex, Tag } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
...@@ -12,7 +12,7 @@ import TextAd from 'ui/shared/ad/TextAd'; ...@@ -12,7 +12,7 @@ import TextAd from 'ui/shared/ad/TextAd';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
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/Tabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
......
...@@ -2,17 +2,19 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,17 +2,19 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useHasAccount from 'lib/hooks/useHasAccount';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
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/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
import TxsTabSlot from 'ui/txs/TxsTabSlot'; import TxsTabSlot from 'ui/txs/TxsTabSlot';
import TxsWatchlist from 'ui/txs/TxsWatchlist';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -24,27 +26,49 @@ const Transactions = () => { ...@@ -24,27 +26,49 @@ const Transactions = () => {
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined'; const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
const txsQuery = useQueryWithPages({ const txsQuery = useQueryWithPages({
resourceName: filter === 'validated' ? 'txs_validated' : 'txs_pending', resourceName: router.query.tab === 'pending' ? 'txs_pending' : 'txs_validated',
filters: { filter }, filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: {
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
},
});
const txsWatchlistQuery = useQueryWithPages({
resourceName: 'txs_watchlist',
options: {
enabled: router.query.tab === 'watchlist',
},
}); });
const { num, socketAlert } = useNewTxsSocket(); const { num, socketAlert } = useNewTxsSocket();
const isFirstPage = txsQuery.pagination.page === 1; const hasAccount = useHasAccount();
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ {
id: 'validated', id: 'validated',
title: verifiedTitle, title: verifiedTitle,
component: <TxsContent query={ txsQuery } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> }, component: <TxsContent query={ txsQuery } showSocketInfo={ txsQuery.pagination.page === 1 } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
{ {
id: 'pending', id: 'pending',
title: 'Pending', title: 'Pending',
component: <TxsContent query={ txsQuery } showBlockInfo={ false } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/>, component: (
<TxsContent
query={ txsQuery }
showBlockInfo={ false }
showSocketInfo={ txsQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
/>
),
}, },
]; hasAccount ? {
id: 'watchlist',
title: 'Watch list',
component: <TxsWatchlist query={ txsWatchlistQuery }/>,
} : undefined,
].filter(Boolean);
return ( return (
<Page> <Page>
......
import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router';
import React, { useEffect, useRef } from 'react';
import type { RoutedTab } from './types';
import TabsWithScroll from './TabsWithScroll';
interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>;
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode;
stickyEnabled?: boolean;
className?: string;
}
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
const router = useRouter();
let tabIndex = 0;
const tabFromRoute = router.query.tab;
if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
if (tabIndex < 0) {
tabIndex = 0;
}
}
const tabsRef = useRef<HTMLDivElement>(null);
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
undefined,
{ shallow: true },
);
}, [ tabs, router ]);
useEffect(() => {
if (router.query.scroll_to_tabs) {
tabsRef?.current?.scrollIntoView(true);
delete router.query.scroll_to_tabs;
router.push(
{
pathname: router.pathname,
query: router.query,
},
undefined,
{ shallow: true },
);
}
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<TabsWithScroll
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ rightSlot }
stickyEnabled={ stickyEnabled }
onTabChange={ handleTabChange }
defaultTabIndex={ tabIndex }
{ ...themeProps }
/>
);
};
export default React.memo(chakra(RoutedTabs));
...@@ -10,13 +10,13 @@ import { Popover, ...@@ -10,13 +10,13 @@ import { Popover,
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react'; import React from 'react';
import type { MenuButton, RoutedTab } from './types'; import type { MenuButton, TabItem } from './types';
import { menuButton } from './utils'; import { menuButton } from './utils';
interface Props { interface Props {
tabs: Array<RoutedTab | MenuButton>; tabs: Array<TabItem | MenuButton>;
activeTab?: RoutedTab; activeTab?: TabItem;
tabsCut: number; tabsCut: number;
isActive: boolean; isActive: boolean;
styles?: StyleProps; styles?: StyleProps;
...@@ -25,7 +25,7 @@ interface Props { ...@@ -25,7 +25,7 @@ interface Props {
size: ButtonProps['size']; size: ButtonProps['size'];
} }
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => { const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => { const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
...@@ -69,4 +69,4 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe ...@@ -69,4 +69,4 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
); );
}; };
export default React.memo(RoutedTabsMenu); export default React.memo(TabsMenu);
import type { LazyMode } from '@chakra-ui/lazy-utils';
import type { ChakraProps, ThemingProps } from '@chakra-ui/react'; import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import { import {
Tab, Tab,
...@@ -10,17 +11,15 @@ import { ...@@ -10,17 +11,15 @@ import {
chakra, chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { RoutedTab } from './types'; import type { TabItem } from './types';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky'; import useIsSticky from 'lib/hooks/useIsSticky';
import RoutedTabsMenu from './RoutedTabsMenu'; import TabsMenu from './TabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs'; import useAdaptiveTabs from './useAdaptiveTabs';
const hiddenItemStyles: StyleProps = { const hiddenItemStyles: StyleProps = {
...@@ -31,18 +30,29 @@ const hiddenItemStyles: StyleProps = { ...@@ -31,18 +30,29 @@ const hiddenItemStyles: StyleProps = {
}; };
interface Props extends ThemingProps<'Tabs'> { interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>; tabs: Array<TabItem>;
lazyBehavior?: LazyMode;
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps); tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
stickyEnabled?: boolean; stickyEnabled?: boolean;
onTabChange?: (index: number) => void;
defaultTabIndex?: number;
className?: string; className?: string;
} }
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => { const TabsWithScroll = ({
const router = useRouter(); tabs,
lazyBehavior,
tabListProps,
rightSlot,
stickyEnabled,
onTabChange,
defaultTabIndex,
className,
...themeProps
}: Props) => {
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const tabsRef = useRef<HTMLDivElement>(null); const tabsRef = useRef<HTMLDivElement>(null);
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile); const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
...@@ -50,46 +60,14 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -50,46 +60,14 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const listBgColor = useColorModeValue('white', 'black'); const listBgColor = useColorModeValue('white', 'black');
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; onTabChange ? onTabChange(index) : setActiveTabIndex(index);
}, [ onTabChange ]);
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
undefined,
{ shallow: true },
);
}, [ tabs, router ]);
useEffect(() => { useEffect(() => {
if (router.query.scroll_to_tabs) { if (defaultTabIndex !== undefined) {
tabsRef?.current?.scrollIntoView(true); setActiveTabIndex(defaultTabIndex);
delete router.query.scroll_to_tabs;
router.push(
{
pathname: router.pathname,
query: router.query,
},
undefined,
{ shallow: true },
);
} }
// replicate componentDidMount }, [ defaultTabIndex ]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
const tabFromRoute = router.query.tab;
if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
if (tabIndex < 0) {
tabIndex = 0;
}
}
setActiveTabIndex(tabIndex);
}
}, [ tabs, router, activeTabIndex ]);
useEffect(() => { useEffect(() => {
if (activeTabIndex < tabs.length && isMobile) { if (activeTabIndex < tabs.length && isMobile) {
...@@ -97,7 +75,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -97,7 +75,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const activeTabRef = tabsRefs[activeTabIndex]; const activeTabRef = tabsRefs[activeTabIndex];
if (activeTabRef.current && listRef.current) { if (activeTabRef.current && listRef.current) {
const activeTabRect = activeTabRef.current.getBoundingClientRect(); const activeTabRect = activeTabRef.current.getBoundingClientRect();
listRef.current.scrollTo({ listRef.current.scrollTo({
left: activeTabRect.left + listRef.current.scrollLeft - 16, left: activeTabRect.left + listRef.current.scrollLeft - 16,
behavior: 'smooth', behavior: 'smooth',
...@@ -125,6 +102,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -125,6 +102,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
position="relative" position="relative"
size={ themeProps.size || 'md' } size={ themeProps.size || 'md' }
ref={ tabsRef } ref={ tabsRef }
lazyBehavior={ lazyBehavior }
> >
<TabList <TabList
marginBottom={{ base: 6, lg: 8 }} marginBottom={{ base: 6, lg: 8 }}
...@@ -160,7 +138,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -160,7 +138,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
{ tabsList.map((tab, index) => { { tabsList.map((tab, index) => {
if (!tab.id) { if (!tab.id) {
return ( return (
<RoutedTabsMenu <TabsMenu
key="menu" key="menu"
tabs={ tabs } tabs={ tabs }
activeTab={ tabs[activeTabIndex] } activeTab={ tabs[activeTabIndex] }
...@@ -201,4 +179,4 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -201,4 +179,4 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
); );
}; };
export default React.memo(chakra(RoutedTabs)); export default React.memo(chakra(TabsWithScroll));
import type React from 'react'; import type React from 'react';
export interface RoutedTab { export interface TabItem {
id: string; id: string;
title: string | (() => React.ReactNode); title: string | (() => React.ReactNode);
component: React.ReactNode; component: React.ReactNode;
subTabs?: Array<string>;
} }
export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>; export type RoutedTab = TabItem & { subTabs?: Array<string> }
export type RoutedSubTab = Omit<TabItem, 'subTabs'>;
export interface MenuButton { export interface MenuButton {
id: null; id: null;
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react'; import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types'; import type { RoutedTab } from '../Tabs/types';
interface Props { interface Props {
className?: string; className?: string;
......
...@@ -6,6 +6,7 @@ import chevronIcon from 'icons/arrows/east-mini.svg'; ...@@ -6,6 +6,7 @@ import chevronIcon from 'icons/arrows/east-mini.svg';
import testnetIcon from 'icons/testnet.svg'; import testnetIcon from 'icons/testnet.svg';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useHasAccount from 'lib/hooks/useHasAccount';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
...@@ -28,11 +29,10 @@ const NavigationDesktop = () => { ...@@ -28,11 +29,10 @@ const NavigationDesktop = () => {
isNavBarCollapsed = false; isNavBarCollapsed = false;
} }
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const hasAccount = hasAuth && appConfig.isAccountSupported; const hasAccount = useHasAccount();
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed); const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
......
...@@ -2,9 +2,8 @@ import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/rea ...@@ -2,9 +2,8 @@ import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/rea
import { animate, motion, useMotionValue } from 'framer-motion'; import { animate, motion, useMotionValue } from 'framer-motion';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies'; import useHasAccount from 'lib/hooks/useHasAccount';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import NavFooter from 'ui/snippets/navigation/NavFooter'; import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
...@@ -30,8 +29,7 @@ const NavigationMobile = () => { ...@@ -30,8 +29,7 @@ const NavigationMobile = () => {
animate(subX, 250, { ease: 'easeInOut', onComplete: () => setOpenedGroupIndex(-1) }); animate(subX, 250, { ease: 'easeInOut', onComplete: () => setOpenedGroupIndex(-1) });
}, [ mainX, subX ]); }, [ mainX, subX ]);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const hasAccount = useHasAccount();
const hasAccount = appConfig.isAccountSupported && isAuth;
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
......
...@@ -2,7 +2,7 @@ import { Box, Tag, Icon } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box, Tag, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg'; import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
...@@ -15,8 +15,8 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -15,8 +15,8 @@ import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
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 RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
......
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TxsResponse } from 'types/api/transaction';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
type QueryResult = UseQueryResult<TxsResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
type Props = {
query: QueryResult;
}
const TxsWatchlist = ({ query }: Props) => {
useRedirectForInvalidAuthToken();
return <TxsContent query={ query } showSocketInfo={ false }/>;
};
export default TxsWatchlist;
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