Commit 04f67442 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into feat/verified-tokens

parents 459062ab f8c75e4a
......@@ -13,7 +13,7 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# network config
......
......@@ -3,7 +3,7 @@ NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg, \#235643 1.5%, \#16191E 77.77%)
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/poa.svg
......
......@@ -12,7 +12,7 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_FEATURED_NETWORKS=
......
......@@ -16,8 +16,6 @@ blockscout:
_default: ENC[AES256_GCM,data:prh45OKeStfh+hPRZLvN981E+yPjjN1E0cI2uIMT+28=,iv:/ZrYd29B2LUZn2s55w/tWmDURDvQwdIDl8JZl8dQhxY=,tag:n2S7OYoag+kI3rNrnmSlTg==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:bgP4VwZ91eMzJVQ3/+fkqNCwBIAv3P12qRoGWu9QkfCKpj3e+Dwt1qjsYV1iNkcjQAJx3jOpVGlsbgdcBGFzHw==,iv:gpa6tbkxHv56wwI3Owdnr5MArJYdiVO3A0UMOsrgCls=,tag:CIgupR1ZNjsVtFYVG2OtHA==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:+EoMWtXdJA5DnRprCaps38VbQlLcJikKvB7FA0VMhaw+aq1d/rh5yIxl/CQBIo2eO8EESoVz6eUjzvgchbmv2IaiGgG3DnZx,iv:nfPmwEZMXM2SOIZ3OMr5u7GTbX6ni5VuFpn5SyHKMfE=,tag:4ePZ5NZ+rdBZf8xWDfEbzA==,type:str]
ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:i08k9qiHA/31nyAri5pIm8MqCUZXLWjvQgcYpAyrcsToszt/L+QsYVcdCEerI0udtd2gJvLuRz3k8GcpZ9OfQB2Y0kJn,iv:Pt3rg7GjhfDw5S4VV9HpLSDsO4AhXlGIsNhdc5rYqCQ=,tag:zL/bLVgSS41kgnv69GxLMg==,type:str]
ACCOUNT_SENDGRID_SENDER:
......@@ -88,8 +86,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-04-27T16:27:47Z"
mac: ENC[AES256_GCM,data:TUIxPt/HMLzkWlPdBcM2mKGlNU+6ob+EWvtfKWAHZaVhH2qwoi+HI+Ncx+j3H9/MR6q9Uhhr611s8rCF0LdwbsK+LGdOBBql+45vfmCE1XRl/DS0LFdZxkZn4ub49C9Uf8g0DOVt02NJQNh3ZNaBa+l2nIirh643hLvzFCCqBM8=,iv:tqRFfzSk0W+NzEADzIWy08aUnpJgcIPBHhUhM0Kf+VA=,tag:0IiapCTs0UITa38cwVyZqQ==,type:str]
lastmodified: "2023-05-09T10:35:44Z"
mac: ENC[AES256_GCM,data:r+iWj2hSNkaAzn3duafu6mUloN5sxIcHf2oW6IpYpNrQzi/oFe509iutYqIZ9r0AOygckcyJkjgNq//ki7SZr+7texfuIGYTfH0371b67ltUuiSgdfv7ONzwr7hA7M+qqOdyvPXqti6yNSrbCSlVec1ph95EKUAlMvqDxh3/wOs=,iv:QtsiyCaNgc1YwQA3p3fT8GRI4f8AA216JcMNyK+3PlY=,tag:nogyCZ+qsMEuuHVdGucjrg==,type:str]
pgp:
- created_at: "2022-09-22T09:52:10Z"
enc: |
......
......@@ -5,7 +5,7 @@ blockscout:
app: blockscout
enabled: true
image:
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-465ba09e
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.4-prerelease-a8c134e4
replicas:
app: 1
# init container
......@@ -398,7 +398,7 @@ frontend:
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
_default: "true"
_default: true
NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
......
......@@ -34,7 +34,7 @@ blockscout:
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
API_SENSITIVE_ENDPOINTS_KEY:
_default: ENC[AES256_GCM,data:cghX4B0yt18xT8kd/buPd6L/zLo/jIZzyVSDPoDGHiDMXoQRxHuHtITQbCjD+w0UdFZqDvDvAC6VUc5veCfITw==,iv:h9iTyEEhxSmStgA8ycncELR1A6AcWezk+zYIyo5Fe/Q=,tag:cC/ye9+Z9MvNR3h5o7JBJg==,type:str]
_default: ENC[AES256_GCM,data:QYhtvpV8sv38+UArgk3bfR6Au+s65PJvQn3AZRLk8loAKe/BurTvtZ81bu0vH51jZVvxc/gAcnocim4j5Y+fvA==,iv:Nktux8P7bskPWgRGnDbAUCz1xJjKy7Brdc7EDoG3e+A=,tag:kbm3LVO42B2fygWcVVsc6g==,type:str]
ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
ACCOUNT_REDIS_URL:
......@@ -44,13 +44,7 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
_default: ENC[AES256_GCM,data:JkZ/gfdXn/8F5GDFROfR+8eMLGETzmkNhwcgCq118QThwTPYtLt6t4Wxa0fs5STJI/v15w8q+xAptHHUxtKJEg==,iv:I+XyuB8eZR+IpKIGmjLon1iFbJssVaDYpusQzjyRiec=,tag:APcnH6F4sM4uuRZdziUgQw==,type:str]
RE_CAPTCHA_SECRET_KEY:
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
RE_CAPTCHA_CLIENT_KEY:
......@@ -155,8 +149,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-05-09T16:11:07Z"
mac: ENC[AES256_GCM,data:JHnHHGNOw84O5No7UDTLSGnLwUJHUUZaY/UBPetpa99OFDMlzFvEnjIh4DjIqhVLXR5DBZJr7BTibQLyo6y/aXlWKDxHvLklLGunKQaQ1Xl/JzDKRTYlAs/JsJRR0ElzzyPmbCKbSJOX0tAtNAuhH6EFKYg+HLYmPHtYcvM1Kck=,iv:OsKv7efW0xJ4ZT/38Uyq0feqVC4i2x4srzG+mqd2Avg=,tag:MWfjO8r3iJSMGluqIHc9jA==,type:str]
lastmodified: "2023-05-10T12:56:02Z"
mac: ENC[AES256_GCM,data:s/AX12T/dTpTcStmR5lcOtUw6AngvauXdDt+56A3wMoJwWSeWm5lVkVK9d/moAZgmuMv5Uji6pVVzohp8QFAoyQvGJdd8eFUqaYDkm6oGkuJidzLGq9qsbJL89bQ7R2hZ7A8JlamgXE0jJmkgsdKQlkjOH2gMdfikaSujPY1kUM=,iv:HKCiMhApXmEfNiUR0oEXupG/atiw3xhEckCRl63AHBE=,tag:76KiFG2GuwRz7Cn3/CO4/g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -359,7 +359,7 @@ frontend:
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
_default: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_API_SPEC_URL:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
......
......@@ -126,7 +126,7 @@ frontend:
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
_default: "true"
_default: true
NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
......
......@@ -114,7 +114,7 @@ frontend:
_default: https://blockscout.com/auth/logout
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
_default: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com
NEXT_PUBLIC_NETWORK_RPC_URL:
......
......@@ -41,7 +41,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; the login path (`/auth/auth0`) will be added to it at execution time. Required if account is supported for the app instance. | - | - | `https://blockscout.com` |
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` | Account logout return url. Required if account is supported for the app instance. | - | - | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
......
......@@ -51,11 +51,11 @@ import type {
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
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 { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config';
......@@ -186,6 +186,11 @@ export const RESOURCES = {
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' 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: {
path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ],
......@@ -221,6 +226,9 @@ export const RESOURCES = {
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [],
},
withdrawals_counters: {
path: '/api/v2/withdrawals/counters',
},
// ADDRESSES
addresses: {
......@@ -417,6 +425,9 @@ export const RESOURCES = {
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist',
},
homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status',
},
......@@ -537,7 +548,7 @@ export interface ResourceError<T = unknown> {
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' |
'txs_validated' | 'txs_pending' | 'txs_watchlist' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
......@@ -568,6 +579,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters :
......@@ -579,6 +591,7 @@ Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'txs_watchlist' ? TransactionsResponseWatchlist :
Q extends 'tx' ? Transaction :
Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx :
......@@ -620,6 +633,7 @@ Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'l2_output_roots' ? L2OutputRootsResponse :
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
Q extends 'l2_deposits' ? L2DepositsResponse :
......
......@@ -32,6 +32,7 @@ export function app(): CspDev.DirectiveDescriptor {
appConfig.api.endpoint,
appConfig.api.socket,
appConfig.statsApi.endpoint,
appConfig.visualizeApi.endpoint,
// chain RPC server
appConfig.network.rpcUrl,
......
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 {
const accountNavItems = [
{
text: 'Watchlist',
text: 'Watch list',
nextRoute: { pathname: '/account/watchlist' as const },
icon: watchlistIcon,
isActive: pathname === '/account/watchlist',
......
......@@ -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)
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 apiToken = req.cookies.get(NAMES.API_TOKEN);
......
import type { AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo';
import * as tokenInstance from 'mocks/tokens/tokenInstance';
export const erc20a: AddressTokenBalance = {
token: tokens.tokenInfoERC20a,
......@@ -59,24 +60,28 @@ export const erc721LongSymbol: AddressTokenBalance = {
export const erc1155a: AddressTokenBalance = {
token: tokens.tokenInfoERC1155a,
token_id: '42',
token_instance: tokenInstance.base,
value: '24',
};
export const erc1155b: AddressTokenBalance = {
token: tokens.tokenInfoERC1155b,
token_id: '100010000000001',
token_instance: tokenInstance.base,
value: '11',
};
export const erc1155withoutName: AddressTokenBalance = {
token: tokens.tokenInfoERC1155WithoutName,
token_id: '64532245',
token_instance: tokenInstance.base,
value: '42',
};
export const erc1155LongId: AddressTokenBalance = {
token: tokens.tokenInfoERC1155b,
token_id: '483200961027732618117991942553110860267520',
token_instance: tokenInstance.base,
value: '42',
};
......
export const data = {
items: [
{
amount: '192175',
amount: '192175000000000',
block_number: 43242,
index: 11688,
receiver: {
......@@ -15,7 +15,7 @@ export const data = {
validator_index: 49622,
},
{
amount: '192175',
amount: '192175000000000',
block_number: 43242,
index: 11687,
receiver: {
......@@ -29,7 +29,7 @@ export const data = {
validator_index: 49621,
},
{
amount: '182773',
amount: '182773000000000',
block_number: 43242,
index: 11686,
receiver: {
......
......@@ -5,7 +5,7 @@ import React from 'react';
import MarketplaceApp from 'ui/pages/MarketplaceApp';
import Page from 'ui/shared/Page/Page';
const AppPage: NextPage = () => {
const MarketplaceAppPage: NextPage = () => {
return (
<Page wrapChildren={ false }>
<Head><title>Blockscout | Marketplace</title></Head>
......@@ -14,6 +14,6 @@ const AppPage: NextPage = () => {
);
};
export default AppPage;
export default MarketplaceAppPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -2,21 +2,21 @@ import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Apps from 'ui/pages/Apps';
import Marketplace from 'ui/pages/Marketplace';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage: NextPage = () => {
const MarketplacePage: NextPage = () => {
return (
<Page>
<PageTitle text="Apps"/>
<PageTitle text="Marketplace"/>
<Head><title>Blockscout | Marketplace</title></Head>
<Apps/>
<Marketplace/>
</Page>
);
};
export default AppsPage;
export default MarketplacePage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './token';
import type { TokenInfo, TokenInstance, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address {
......@@ -48,6 +48,7 @@ export interface AddressTokenBalance {
token: TokenInfo;
token_id: string | null;
value: string;
token_instance?: TokenInstance;
}
export interface AddressTokensResponse {
......
......@@ -70,6 +70,15 @@ export interface TransactionsResponsePending {
} | 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 TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
......@@ -16,3 +16,8 @@ export type WithdrawalsItem = {
timestamp: string;
validator_index: number;
}
export type WithdrawalsCounters = {
withdrawal_count: string;
withdrawal_sum: string;
}
export type AppItemPreview = {
export type MarketplaceAppPreview = {
id: string;
external?: boolean;
title: string;
logo: string;
logoDarkMode?: string;
shortDescription: string;
categories: Array<string>;
url: string;
}
export type AppItemOverview = AppItemPreview & {
export type MarketplaceAppOverview = MarketplaceAppPreview & {
author: string;
description: string;
site?: string;
......@@ -17,7 +18,7 @@ export type AppItemOverview = AppItemPreview & {
github?: string;
}
export enum AppCategory {
export enum MarketplaceCategory {
ALL = 'All apps',
FAVORITES = 'Favorites',
}
import React from 'react';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
import type { RoutedSubTab } from 'ui/shared/Tabs/types';
import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
interface Props {
......
......@@ -9,7 +9,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address';
import Pagination 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 ERC20Tokens from './tokens/ERC20Tokens';
......
......@@ -4,13 +4,13 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import NftImage from 'ui/shared/nft/NftImage';
import NftMedia from 'ui/shared/nft/NftMedia';
import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance;
const NFTItem = ({ token, token_id: tokenId }: Props) => {
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Props) => {
const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } });
return (
......@@ -26,11 +26,10 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }>
<NftImage
<NftMedia
mb="18px"
url={ null }
fallbackPadding="30px"
cursor="pointer"
imageUrl={ tokenInstance?.image_url || null }
animationUrl={ tokenInstance?.animation_url || null }
/>
</LinkOverlay>
{ tokenId && (
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
......@@ -13,11 +12,29 @@ export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('default view', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp>
<LatestTxs/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
});
test('default view +@dark-mode', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
......@@ -47,10 +64,6 @@ test.describe('socket', () => {
};
test('new item', async({ mount, page, createSocket }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
......
......@@ -11,6 +11,7 @@ const LatestTxsItemSkeleton = () => {
return (
<Box
width="100%"
minW="700px"
borderTop="1px solid"
borderColor="divider"
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 appConfig from 'configs/app/config';
import useHasAccount from 'lib/hooks/useHasAccount';
import LatestDeposits from 'ui/home/LatestDeposits';
import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
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 (
<>
<Heading as="h4" size="sm" mb={ 4 }>Transactions</Heading>
<Tabs isLazy lazyBehavior="keepMounted" defaultIndex={ 0 } variant="soft-rounded">
<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>
<TabsWithScroll tabs={ tabs } lazyBehavior="keepMounted"/>
</>
);
}
......
......@@ -5,7 +5,7 @@ import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market';
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap';
export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId;
......
......@@ -46,7 +46,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
};
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'market_cup',
id: 'market_cap',
title: 'Market cap',
value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }),
icon: <Icon as={ globeIcon } boxSize={ 6 } bgColor="#6A5DCC" borderRadius="base" color="white"/>,
......
......@@ -2,26 +2,27 @@ import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorMod
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
import type { AppItemPreview } from 'types/client/apps';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import AppCardLink from './AppCardLink';
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
interface Props extends AppItemPreview {
interface Props extends MarketplaceAppPreview {
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppCard = ({
const MarketplaceAppCard = ({
id,
url,
external,
title,
logo,
logoDarkMode,
shortDescription,
categories,
onInfoClick,
......@@ -39,6 +40,8 @@ const AppCard = ({
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
return (
<LinkBox
_hover={{
......@@ -73,7 +76,7 @@ const AppCard = ({
justifyContent="center"
>
<Image
src={ logo }
src={ logoUrl }
alt={ `${ title } app icon` }
/>
</Box>
......@@ -85,7 +88,7 @@ const AppCard = ({
size={{ base: 'xs', sm: 'sm' }}
fontWeight="semibold"
>
<AppCardLink
<MarketplaceAppCardLink
id={ id }
url={ url }
external={ external }
......@@ -158,4 +161,4 @@ const AppCard = ({
);
};
export default React.memo(AppCard);
export default React.memo(MarketplaceAppCard);
......@@ -9,7 +9,7 @@ type Props = {
title: string;
}
const AppLink = ({ url, external, id, title }: Props) => {
const MarketplaceAppCardLink = ({ url, external, id, title }: Props) => {
return external ? (
<LinkOverlay href={ url } isExternal={ true }>
{ title }
......@@ -23,4 +23,4 @@ const AppLink = ({ url, external, id, title }: Props) => {
);
};
export default AppLink;
export default MarketplaceAppCardLink;
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
export const AppCardSkeleton = () => {
const MarketplaceAppCardSkeleton = () => {
return (
<Box
borderRadius="md"
......@@ -43,3 +43,5 @@ export const AppCardSkeleton = () => {
</Box>
);
};
export default MarketplaceAppCardSkeleton;
import {
Box, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, useColorModeValue,
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg';
......@@ -15,16 +15,16 @@ import starOutlineIcon from 'icons/star_outline.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import AppModalLink from './AppModalLink';
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
type Props = {
onClose: () => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
data: AppItemOverview;
data: MarketplaceAppOverview;
}
const AppModal = ({
const MarketplaceAppModal = ({
onClose,
isFavorite,
onFavoriteClick,
......@@ -41,6 +41,7 @@ const AppModal = ({
telegram,
twitter,
logo,
logoDarkMode,
categories,
} = data;
......@@ -64,6 +65,7 @@ const AppModal = ({
}, [ onFavoriteClick, data.id, isFavorite ]);
const isMobile = useIsMobile();
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
return (
<Modal
......@@ -89,7 +91,7 @@ const AppModal = ({
gridRow={{ base: '1 / 3', sm: '1 / 4' }}
>
<Image
src={ logo }
src={ logoUrl }
alt={ `${ title } app icon` }
/>
</Flex>
......@@ -120,7 +122,7 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }}
>
<Box display="flex">
<AppModalLink
<MarketplaceAppModalLink
id={ data.id }
url={ url }
external={ external }
......@@ -241,4 +243,4 @@ const AppModal = ({
);
};
export default AppModal;
export default MarketplaceAppModal;
......@@ -9,7 +9,7 @@ type Props = {
title: string;
}
const AppModalLink = ({ url, external, id }: Props) => {
const MarketplaceAppModalLink = ({ url, external, id }: Props) => {
const buttonProps = {
size: 'sm',
marginRight: 2,
......@@ -36,4 +36,4 @@ const AppModalLink = ({ url, external, id }: Props) => {
);
};
export default AppModalLink;
export default MarketplaceAppModalLink;
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import React from 'react';
import { AppCategory } from 'types/client/apps';
import { MarketplaceCategory } from 'types/client/marketplace';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
import MarketplaceCategoriesMenuItem from './MarketplaceCategoriesMenuItem';
type Props = {
categories: Array<string>;
......@@ -13,10 +13,10 @@ type Props = {
onSelect: (category: string) => void;
}
const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
const options = React.useMemo(() => ([
AppCategory.FAVORITES,
AppCategory.ALL,
MarketplaceCategory.FAVORITES,
MarketplaceCategory.ALL,
...categories,
]), [ categories ]);
......@@ -43,7 +43,7 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) =>
<MenuList zIndex={ 3 }>
{ options.map((category: string) => (
<CategoriesMenuItem
<MarketplaceCategoriesMenuItem
key={ category }
id={ category }
onClick={ onSelect }
......@@ -54,4 +54,4 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) =>
);
};
export default React.memo(CategoriesMenu);
export default React.memo(MarketplaceCategoriesMenu);
......@@ -2,7 +2,7 @@ import { Icon, MenuItem } from '@chakra-ui/react';
import type { FunctionComponent, SVGAttributes } from 'react';
import React, { useCallback } from 'react';
import { AppCategory } from 'types/client/apps';
import { MarketplaceCategory } from 'types/client/marketplace';
import starFilledIcon from 'icons/star_filled.svg';
......@@ -12,10 +12,10 @@ type Props = {
}
const ICONS: Record<string, FunctionComponent<SVGAttributes<SVGElement>>> = {
[AppCategory.FAVORITES]: starFilledIcon,
[MarketplaceCategory.FAVORITES]: starFilledIcon,
};
const CategoriesMenuItem = ({ id, onClick }: Props) => {
const MarketplaceCategoriesMenuItem = ({ id, onClick }: Props) => {
const handleSelection = useCallback(() => {
onClick(id);
}, [ id, onClick ]);
......@@ -33,4 +33,4 @@ const CategoriesMenuItem = ({ id, onClick }: Props) => {
);
};
export default CategoriesMenuItem;
export default MarketplaceCategoriesMenuItem;
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { AppItemPreview } from 'types/client/apps';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { apos } from 'lib/html-entities';
import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import MarketplaceAppCard from './MarketplaceAppCard';
type Props = {
apps: Array<AppItemPreview>;
apps: Array<MarketplaceAppPreview>;
onAppClick: (id: string) => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
return apps.length > 0 ? (
<Grid
templateColumns={{
......@@ -28,13 +29,14 @@ const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) =>
<GridItem
key={ app.id }
>
<AppCard
<MarketplaceAppCard
onInfoClick={ onAppClick }
id={ app.id }
external={ app.external }
url={ app.url }
title={ app.title }
logo={ app.logo }
logoDarkMode={ app.logoDarkMode }
shortDescription={ app.shortDescription }
categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
......@@ -48,4 +50,4 @@ const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) =>
);
};
export default React.memo(AppList);
export default React.memo(MarketplaceList);
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
import MarketplaceAppCardSkeleton from './MarketplaceAppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const AppListSkeleton = () => {
const MarketplaceListSkeleton = () => {
return (
<Grid
templateColumns={{
......@@ -19,11 +19,11 @@ const AppListSkeleton = () => {
<GridItem
key={ index }
>
<AppCardSkeleton/>
<MarketplaceAppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default AppListSkeleton;
export default MarketplaceListSkeleton;
import { useQuery } from '@tanstack/react-query';
import _pickBy from 'lodash/pickBy';
import _unique from 'lodash/uniq';
import React, { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import React from 'react';
import type { AppItemOverview } from 'types/client/apps';
import { AppCategory } from 'types/client/apps';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString';
const favoriteAppsLocalStorageKey = 'favoriteApps';
......@@ -20,31 +23,36 @@ function getFavoriteApps() {
}
}
function isAppNameMatches(q: string, app: AppItemOverview) {
function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: string, app: AppItemOverview, favoriteApps: Array<string>) {
return category === AppCategory.ALL ||
(category === AppCategory.FAVORITES && favoriteApps.includes(app.id)) ||
function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array<string>) {
return category === MarketplaceCategory.ALL ||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps() {
const [ selectedAppId, setSelectedAppId ] = useState<string | null>(null);
const [ selectedCategoryId, setSelectedCategoryId ] = useState<string>(AppCategory.ALL);
const [ filterQuery, setFilterQuery ] = useState('');
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
export default function useMarketplace() {
const router = useRouter();
const defaultCategoryId = getQueryParamString(router.query.category);
const defaultFilterQuery = getQueryParamString(router.query.filter);
const [ selectedAppId, setSelectedAppId ] = React.useState<string | null>(null);
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const apiFetch = useApiFetch();
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<AppItemOverview>>(
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
{
select: (data) => (data as Array<AppItemOverview>).sort((a, b) => a.title.localeCompare(b.title)),
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
staleTime: Infinity,
});
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
......@@ -58,14 +66,14 @@ export default function useMarketplaceApps() {
}
}, [ ]);
const showAppInfo = useCallback((id: string) => {
const showAppInfo = React.useCallback((id: string) => {
setSelectedAppId(id);
}, []);
const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = useCallback(() => setSelectedAppId(null), []);
const clearSelectedAppId = React.useCallback(() => setSelectedAppId(null), []);
const handleCategoryChange = useCallback((newCategory: string) => {
const handleCategoryChange = React.useCallback((newCategory: string) => {
setSelectedCategoryId(newCategory);
}, []);
......@@ -77,13 +85,38 @@ export default function useMarketplaceApps() {
return _unique(data?.map(app => app.categories).flat()) || [];
}, [ data ]);
useEffect(() => {
React.useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
React.useEffect(() => {
if (!isLoading && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
}
// run only when data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isLoading ]);
React.useEffect(() => {
const query = _pickBy({
category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId,
filter: debouncedFilterQuery,
}, Boolean);
router.replace(
{ pathname: '/apps', query },
undefined,
{ shallow: true },
);
// omit router in the deps because router.push() somehow modifies it
// and we get infinite re-renders then
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ debouncedFilterQuery, selectedCategoryId ]);
return React.useMemo(() => ({
selectedCategoryId,
onCategoryChange: handleCategoryChange,
filterQuery: debouncedFilterQuery,
onSearchInputChange: setFilterQuery,
isLoading,
isError,
......@@ -108,5 +141,6 @@ export default function useMarketplaceApps() {
isError,
isLoading,
showAppInfo,
debouncedFilterQuery,
]);
}
......@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react';
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 iconSuccess from 'icons/status/success.svg';
......@@ -24,8 +24,8 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
export const tokenTabsByType: Record<TokenType, string> = {
'ERC-20': 'tokens_erc20',
......
......@@ -2,7 +2,7 @@ import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
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 useApiQuery from 'lib/api/useApiQuery';
......@@ -16,8 +16,8 @@ import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } 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 RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent';
const TAB_LIST_PROPS = {
......
......@@ -2,7 +2,7 @@ import { useRouter } from 'next/router';
import React from 'react';
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 useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -10,7 +10,7 @@ import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
import Page from 'ui/shared/Page/Page';
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> = {
blocks: 'block',
......
......@@ -3,15 +3,15 @@ import React from 'react';
import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import AppModal from 'ui/apps/AppModal';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
import MarketplaceListSkeleton from 'ui/marketplace/MarketplaceListSkeleton';
import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps';
import useMarketplace from '../marketplace/useMarketplace';
const Apps = () => {
const Marketplace = () => {
const {
isLoading,
isError,
......@@ -19,6 +19,7 @@ const Apps = () => {
selectedCategoryId,
categories,
onCategoryChange,
filterQuery,
onSearchInputChange,
showAppInfo,
displayedApps,
......@@ -26,7 +27,7 @@ const Apps = () => {
clearSelectedAppId,
favoriteApps,
onFavoriteClick,
} = useMarketplaceApps();
} = useMarketplace();
if (isError) {
throw new Error('Unable to get apps list', { cause: error });
......@@ -40,17 +41,22 @@ const Apps = () => {
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
<MarketplaceCategoriesMenu
categories={ categories }
selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange }
/>
<FilterInput onChange={ onSearchInputChange } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
<FilterInput
initialValue={ filterQuery }
onChange={ onSearchInputChange }
marginBottom={{ base: '4', lg: '6' }}
placeholder="Find app"
/>
</Box>
{ isLoading ? <AppListSkeleton/> : (
<AppList
{ isLoading ? <MarketplaceListSkeleton/> : (
<MarketplaceList
apps={ displayedApps }
onAppClick={ showAppInfo }
favoriteApps={ favoriteApps }
......@@ -59,7 +65,7 @@ const Apps = () => {
) }
{ selectedApp && (
<AppModal
<MarketplaceAppModal
onClose={ clearSelectedAppId }
isFavorite={ favoriteApps.includes(selectedApp.id) }
onFavoriteClick={ onFavoriteClick }
......@@ -90,4 +96,4 @@ const Apps = () => {
);
};
export default Apps;
export default Marketplace;
......@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
......@@ -26,15 +26,15 @@ const MarketplaceApp = () => {
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, AppItemOverview>(
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ],
async() => {
const result = await apiFetch<Array<AppItemOverview>, unknown>(appConfig.marketplaceConfigUrl || '');
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(appConfig.marketplaceConfigUrl || '');
if (!Array.isArray(result)) {
throw result;
}
const item = result.find((app: AppItemOverview) => app.id === id);
const item = result.find((app: MarketplaceAppOverview) => app.id === id);
if (!item) {
throw { status: 404 };
}
......
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 PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page/Page';
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> = [
{ id: 'address', title: 'Address', component: <PrivateAddressTags/> },
......
......@@ -5,7 +5,7 @@ import React, { useEffect } from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } 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 iconSuccess from 'icons/status/success.svg';
......@@ -26,8 +26,8 @@ import Tag from 'ui/shared/chakra/Tag';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } 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 RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
......
......@@ -2,7 +2,7 @@ import { Flex, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router';
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 { useAppContext } from 'lib/appContext';
......@@ -11,7 +11,7 @@ import TextAd from 'ui/shared/ad/TextAd';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
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 TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
......
......@@ -2,17 +2,19 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
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 useHasAccount from 'lib/hooks/useHasAccount';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page';
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 TxsTabSlot from 'ui/txs/TxsTabSlot';
import TxsWatchlist from 'ui/txs/TxsWatchlist';
const TAB_LIST_PROPS = {
marginBottom: 0,
......@@ -24,27 +26,49 @@ const Transactions = () => {
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
const router = useRouter();
const isMobile = useIsMobile();
const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
const txsQuery = useQueryWithPages({
resourceName: filter === 'validated' ? 'txs_validated' : 'txs_pending',
filters: { filter },
resourceName: router.query.tab === 'pending' ? 'txs_pending' : 'txs_validated',
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 isFirstPage = txsQuery.pagination.page === 1;
const hasAccount = useHasAccount();
const tabs: Array<RoutedTab> = [
{
id: 'validated',
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',
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 (
<Page>
......
......@@ -8,6 +8,7 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import Withdrawals from './Withdrawals';
const WITHDRAWALS_API_URL = buildApiUrl('withdrawals');
const WITHDRAWALS_COUNTERS_API_URL = buildApiUrl('withdrawals_counters');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
......@@ -20,6 +21,11 @@ test('base view +@mobile', async({ mount, page }) => {
body: JSON.stringify(withdrawalsData),
}));
await page.route(WITHDRAWALS_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ withdrawal_count: '111111', withdrawal_sum: '1010101010110101001101010' }),
}));
const component = await mount(
<TestApp>
<Withdrawals/>
......
import { Flex, Hide, Show } from '@chakra-ui/react';
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
......@@ -18,6 +21,8 @@ const Withdrawals = () => {
resourceName: 'withdrawals',
});
const countersQuery = useApiQuery('withdrawals_counters');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show>
......@@ -25,14 +30,43 @@ const Withdrawals = () => {
</>
) : null;
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) : null;
const text = (() => {
if (countersQuery.isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '320px' }}
h="24px"
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError) {
return null;
}
const { valueStr } = getCurrencyValue({ value: countersQuery.data.withdrawal_sum });
return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
{ BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed and { valueStr } ETH withdrawn
</Text>
);
})();
const actionBar = (
<>
{ (isMobile || !isPaginationVisible) && text }
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile && text }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) }
</>
);
return (
<Page>
......
......@@ -24,7 +24,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
}, [ hasCopied ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 1 }/>;
}
return (
......
import { Box, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import DataFetchAlert from './DataFetchAlert';
import SkeletonList from './skeletons/SkeletonList';
......
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,
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
import type { MenuButton, RoutedTab } from './types';
import type { MenuButton, TabItem } from './types';
import { menuButton } from './utils';
interface Props {
tabs: Array<RoutedTab | MenuButton>;
activeTab?: RoutedTab;
tabs: Array<TabItem | MenuButton>;
activeTab?: TabItem;
tabsCut: number;
isActive: boolean;
styles?: StyleProps;
......@@ -25,7 +25,7 @@ interface Props {
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 handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
......@@ -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 {
Tab,
......@@ -10,17 +11,15 @@ import {
chakra,
} from '@chakra-ui/react';
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 type { RoutedTab } from './types';
import type { TabItem } from './types';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import RoutedTabsMenu from './RoutedTabsMenu';
import TabsMenu from './TabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
const hiddenItemStyles: StyleProps = {
......@@ -31,18 +30,29 @@ const hiddenItemStyles: StyleProps = {
};
interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>;
tabs: Array<TabItem>;
lazyBehavior?: LazyMode;
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode;
stickyEnabled?: boolean;
onTabChange?: (index: number) => void;
defaultTabIndex?: number;
className?: string;
}
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
const router = useRouter();
const TabsWithScroll = ({
tabs,
lazyBehavior,
tabListProps,
rightSlot,
stickyEnabled,
onTabChange,
defaultTabIndex,
className,
...themeProps
}: Props) => {
const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0);
const isMobile = useIsMobile();
const tabsRef = useRef<HTMLDivElement>(null);
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
......@@ -50,46 +60,14 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const listBgColor = useColorModeValue('white', 'black');
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 ]);
onTabChange ? onTabChange(index) : setActiveTabIndex(index);
}, [ onTabChange ]);
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 },
);
if (defaultTabIndex !== undefined) {
setActiveTabIndex(defaultTabIndex);
}
// replicate componentDidMount
// 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 ]);
}, [ defaultTabIndex ]);
useEffect(() => {
if (activeTabIndex < tabs.length && isMobile) {
......@@ -97,7 +75,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const activeTabRef = tabsRefs[activeTabIndex];
if (activeTabRef.current && listRef.current) {
const activeTabRect = activeTabRef.current.getBoundingClientRect();
listRef.current.scrollTo({
left: activeTabRect.left + listRef.current.scrollLeft - 16,
behavior: 'smooth',
......@@ -125,6 +102,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
position="relative"
size={ themeProps.size || 'md' }
ref={ tabsRef }
lazyBehavior={ lazyBehavior }
>
<TabList
marginBottom={{ base: 6, lg: 8 }}
......@@ -160,7 +138,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
{ tabsList.map((tab, index) => {
if (!tab.id) {
return (
<RoutedTabsMenu
<TabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTabIndex] }
......@@ -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';
export interface RoutedTab {
export interface TabItem {
id: string;
title: string | (() => 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 {
id: null;
......
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
import type { StaticRoute } from 'nextjs-routes';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -35,7 +36,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => {
return;
}
const url = route({ pathname: '/api/media-type', query: { url: animationUrl } });
const url = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url: animationUrl } });
fetch(url)
.then((response) => response.json())
.then((_data) => {
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types';
import type { RoutedTab } from '../Tabs/types';
interface Props {
className?: string;
......
......@@ -44,7 +44,7 @@ const Burger = () => {
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 } display="flex" flexDirection="column">
<Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/>
{ appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/> }
<Flex alignItems="center" justifyContent="space-between">
<NetworkLogo onClick={ handleNetworkLogoClick }/>
{ appConfig.featuredNetworks ? (
......
......@@ -6,6 +6,7 @@ import chevronIcon from 'icons/arrows/east-mini.svg';
import testnetIcon from 'icons/testnet.svg';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
import useHasAccount from 'lib/hooks/useHasAccount';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
......@@ -28,11 +29,10 @@ const NavigationDesktop = () => {
isNavBarCollapsed = false;
}
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
const { mainNavItems, accountNavItems } = useNavItems();
const hasAccount = hasAuth && appConfig.isAccountSupported;
const hasAccount = useHasAccount();
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed);
const handleTogglerClick = React.useCallback(() => {
......@@ -61,7 +61,7 @@ const NavigationDesktop = () => {
width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Icon as={ testnetIcon } h="14px" w="auto" color="red.400" pl={ 3 } alignSelf="flex-start"/>
{ appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" pl={ 3 } alignSelf="flex-start"/> }
<Box
as="header"
display="flex"
......
......@@ -2,9 +2,8 @@ import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/rea
import { animate, motion, useMotionValue } from 'framer-motion';
import React, { useCallback } from 'react';
import appConfig from 'configs/app/config';
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 NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink';
......@@ -30,8 +29,7 @@ const NavigationMobile = () => {
animate(subX, 250, { ease: 'easeInOut', onComplete: () => setOpenedGroupIndex(-1) });
}, [ mainX, subX ]);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = appConfig.isAccountSupported && isAuth;
const hasAccount = useHasAccount();
const iconColor = useColorModeValue('blue.600', 'blue.300');
......
......@@ -5,9 +5,9 @@ import type { StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats';
import { apos } from 'lib/html-entities';
import ChartWidgetSkeleton from 'ui/shared/chart/ChartWidgetSkeleton';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidgetSkeleton from '../shared/chart/ChartWidgetSkeleton';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
import ChartWidgetContainer from './ChartWidgetContainer';
......
......@@ -2,7 +2,7 @@ import { Box, Tag, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
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 useApiQuery from 'lib/api/useApiQuery';
......@@ -15,8 +15,8 @@ import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination 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 RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
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