Hot contracts table (#3165)
* page layout * add interval filter * fix navigation * value formatting and test * fix: correct empty state message from 'pools' to 'hot contracts' * update screenshots * add disabled state to tag select and fix some bugs * fix screenshot * update select texts
@@ -0,0 +1,21 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Hot contracts';
|
||||
|
||||
const config: Feature<{ isEnabled: true }> = (() => {
|
||||
if (getEnvValue('NEXT_PUBLIC_HOT_CONTRACTS_ENABLED') === 'true') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -22,6 +22,7 @@ export { default as gasTracker } from './gasTracker';
|
||||
export { default as getGasButton } from './getGasButton';
|
||||
export { default as googleAnalytics } from './googleAnalytics';
|
||||
export { default as growthBook } from './growthBook';
|
||||
export { default as hotContracts } from './hotContracts';
|
||||
export { default as marketplace } from './marketplace';
|
||||
export { default as megaEth } from './megaEth';
|
||||
export { default as metasuites } from './metasuites';
|
||||
|
||||
@@ -37,6 +37,7 @@ NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'text_color':['rgba(165, 252, 122, 1)']}
|
||||
NEXT_PUBLIC_HOT_CONTRACTS_ENABLED=true
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
|
||||
@@ -146,6 +146,7 @@ const schema = yup
|
||||
return isUndefined || valueSchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_FLASHBLOCKS_SOCKET_URL: yup.string().test(urlTest),
|
||||
NEXT_PUBLIC_HOT_CONTRACTS_ENABLED: yup.boolean(),
|
||||
|
||||
// Misc
|
||||
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
|
||||
|
||||
@@ -38,6 +38,7 @@ NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED=false
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']}
|
||||
NEXT_PUBLIC_HOT_CONTRACTS_ENABLED=true
|
||||
NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
|
||||
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
|
||||
@@ -988,6 +988,16 @@ This feature enables Blockscout Merits program. It requires that the [My account
|
||||
|
||||
|
||||
|
||||
### Hot contract
|
||||
|
||||
Show the page with aggregate metrics for the most popular contracts.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_HOT_CONTRACTS_ENABLED | `boolean` | Set to true to enable the feature | Required | - | `true` | upcoming |
|
||||
|
||||
|
||||
|
||||
### Flashblocks
|
||||
|
||||
This feature allows users to view [Flashblocks](https://docs.base.org/base-chain/flashblocks/apps)-related content in the explorer, including the Flashblocks real-time feed. It currently supports only Base chains.
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#gas_svg__a)">
|
||||
<path d="M12 17h1v2H0v-2h1V2a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v8h2a2 2 0 0 1 2 2v4a1 1 0 0 0 2 0V9h-2a1 1 0 0 1-1-1V4.414l-1.657-1.657 1.414-1.414 4.95 4.95A.996.996 0 0 1 20 7v9a3 3 0 0 1-6 0v-4h-2v5Zm-9 0h7v-6H3v6ZM3 3v6h7V3H3Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="gas_svg__a">
|
||||
<path fill="#fff" d="M0 0h20v20H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.727 5A2.742 2.742 0 0 0 5 7.727V25h12.727v-6.364h1.819v3.637A2.742 2.742 0 0 0 22.273 25 2.742 2.742 0 0 0 25 22.273V13.01c0-.724-.284-1.392-.796-1.903l-4.29-4.29-1.278 1.279 2.586 2.585c-.977.415-1.677 1.378-1.677 2.5a2.742 2.742 0 0 0 2.728 2.727c.32 0 .621-.067.909-.17v6.534c0 .515-.394.909-.91.909a.893.893 0 0 1-.908-.91v-3.636a1.83 1.83 0 0 0-1.819-1.818h-1.818v-9.09A2.742 2.742 0 0 0 15 5H7.727Zm0 1.818H15c.515 0 .91.394.91.91v3.636H6.817V7.727c0-.515.394-.909.91-.909Zm14.546 5.455c.511 0 .909.398.909.909s-.398.909-.91.909a.897.897 0 0 1-.908-.91c0-.51.397-.908.909-.908Zm-15.455.909h9.091v10H6.82v-10Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 732 B |
@@ -0,0 +1,10 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#gas_slim_svg__a)">
|
||||
<path d="M12 17h1v2H0v-2h1V2a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v8h2a2 2 0 0 1 2 2v4a1 1 0 0 0 2 0V9h-2a1 1 0 0 1-1-1V4.414l-1.657-1.657 1.414-1.414 4.95 4.95A.996.996 0 0 1 20 7v9a3 3 0 0 1-6 0v-4h-2v5Zm-9 0h7v-6H3v6ZM3 3v6h7V3H3Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="gas_slim_svg__a">
|
||||
<path fill="#fff" d="M0 0h20v20H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m19.094 2.756.03.007a.955.955 0 0 1 .108.014l.129.035.003.001.021.007.063.023a.996.996 0 0 1 .28.185l5.522 5.297.152.146-.016.021a.986.986 0 0 1 .172.556v11.68a2.778 2.778 0 0 1-2.49 2.76l-.282.014h-1.397v.975a2.778 2.778 0 0 1-2.49 2.758l-.283.015H5.442c-.517 0-.944-.393-.995-.898l-.006-.102V7.499c0-.552.45-1 1-1h3.17V3.75l.006-.103a1 1 0 0 1 .995-.897h9.423a.32.32 0 0 1 .06.006ZM6.001 25.69h13.056l.058-.005c.137-.029.302-.157.453-.398.165-.264.262-.585.262-.81v-.975H9.612a1 1 0 0 1-1-1.001V8.059h-2.61V25.69Zm4.17-3.749h13.056l.08-.003a.774.774 0 0 0 .692-.77V9.607h-3.192c-.733 0-1.329-.182-1.74-.591-.41-.41-.592-1.006-.592-1.74V4.31H10.17V21.94Zm7.207-9.186a.66.66 0 0 1 .465.287l1.332 1.999 1.605-1.282.084-.058a.658.658 0 0 1 .666.009l.083.06c.179.151.267.392.22.628l-.866 4.325a.658.658 0 0 1-.527.517l-.115.01H14.27a.656.656 0 0 1-.643-.527l-.865-4.325a.656.656 0 0 1 .302-.688l.09-.046a.658.658 0 0 1 .66.095l1.604 1.282 1.333-1.999a.658.658 0 0 1 .545-.292l.081.005Zm-1.265 3.608a.658.658 0 0 1-.444.285.658.658 0 0 1-.395-.063l-.115-.073-.76-.607.41 2.034h4.979l.406-2.034-.757.607a.658.658 0 0 1-.374.142l-.136-.006a.66.66 0 0 1-.358-.18l-.087-.105-1.185-1.777-1.184 1.777Zm3.922-9.088.004.08c.04.388.37.692.768.692h1.706l-2.478-2.39v1.618Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -8,6 +8,7 @@ import type { Blob } from 'types/api/blobs';
|
||||
import type { Block } from 'types/api/block';
|
||||
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
|
||||
import type { BackendVersionConfig, CeloConfig, CsvExportConfig } from 'types/api/configs';
|
||||
import type { HotContractsFilters, HotContractsResponse, HotContractsSorting } from 'types/api/contracts';
|
||||
import type { DepositsResponse, DepositsCounters } from 'types/api/deposits';
|
||||
import type { CeloEpochDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs';
|
||||
import type { IndexingStatus } from 'types/api/indexingStatus';
|
||||
@@ -75,6 +76,11 @@ export const GENERAL_API_MISC_RESOURCES = {
|
||||
stats_charts_secondary_coin_price: {
|
||||
path: '/api/v2/stats/charts/secondary-coin-market',
|
||||
},
|
||||
stats_hot_contracts: {
|
||||
path: '/api/v2/stats/hot-smart-contracts',
|
||||
paginated: true,
|
||||
filterFields: [ 'scale' as const ],
|
||||
},
|
||||
|
||||
// HOMEPAGE
|
||||
homepage_blocks: {
|
||||
@@ -271,6 +277,7 @@ R extends 'general:stats' ? HomeStats :
|
||||
R extends 'general:stats_charts_txs' ? ChartTransactionResponse :
|
||||
R extends 'general:stats_charts_market' ? ChartMarketResponse :
|
||||
R extends 'general:stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse :
|
||||
R extends 'general:stats_hot_contracts' ? HotContractsResponse :
|
||||
R extends 'general:homepage_blocks' ? Array<Block> :
|
||||
R extends 'general:homepage_txs' ? Array<Transaction> :
|
||||
R extends 'general:homepage_txs_watchlist' ? Array<Transaction> :
|
||||
@@ -316,6 +323,7 @@ never;
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type GeneralApiMiscPaginationFilters<R extends GeneralApiMiscResourceName> =
|
||||
R extends 'general:stats_hot_contracts' ? HotContractsFilters :
|
||||
R extends 'general:search' ? SearchResultFilters :
|
||||
R extends 'general:user_ops' ? UserOpsFilters :
|
||||
R extends 'general:validators_stability' ? ValidatorsStabilityFilters :
|
||||
@@ -325,6 +333,7 @@ never;
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type GeneralApiMiscPaginationSorting<R extends GeneralApiMiscResourceName> =
|
||||
R extends 'general:stats_hot_contracts' ? HotContractsSorting :
|
||||
R extends 'general:validators_stability' ? ValidatorsStabilitySorting :
|
||||
R extends 'general:validators_blackfort' ? ValidatorsBlackfortSorting :
|
||||
never;
|
||||
|
||||
@@ -261,34 +261,34 @@ export default function useNavItems(): ReturnType {
|
||||
].filter(Boolean);
|
||||
|
||||
const statsNavItem = (() => {
|
||||
const uptimeItem = {
|
||||
text: 'Uptime',
|
||||
nextRoute: { pathname: '/uptime' as const },
|
||||
icon: 'refresh_menu',
|
||||
isActive: pathname.startsWith('/uptime'),
|
||||
};
|
||||
const items = [
|
||||
config.features.stats.isEnabled && {
|
||||
text: 'Chain stats',
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'graph',
|
||||
isActive: pathname.startsWith('/stats'),
|
||||
},
|
||||
config.features.megaEth.isEnabled && {
|
||||
text: 'Uptime',
|
||||
nextRoute: { pathname: '/uptime' as const },
|
||||
icon: 'refresh_menu',
|
||||
isActive: pathname.startsWith('/uptime'),
|
||||
},
|
||||
config.features.hotContracts.isEnabled && {
|
||||
text: 'Hot contracts',
|
||||
nextRoute: { pathname: '/hot-contracts' as const },
|
||||
icon: 'hot-contracts',
|
||||
isActive: pathname.startsWith('/hot-contracts'),
|
||||
},
|
||||
config.features.gasTracker.isEnabled && {
|
||||
text: 'Gas tracker',
|
||||
nextRoute: { pathname: '/gas-tracker' as const },
|
||||
icon: 'gas',
|
||||
isActive: pathname.startsWith('/gas-tracker'),
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
if (config.features.stats.isEnabled && config.features.megaEth.isEnabled) {
|
||||
return {
|
||||
text: 'Charts & stats',
|
||||
icon: 'stats',
|
||||
isActive: pathname.startsWith('/stats') || pathname.startsWith('/uptime'),
|
||||
subItems: [
|
||||
{
|
||||
text: `${ config.chain.name } stats`,
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'graph',
|
||||
isActive: pathname.startsWith('/stats/'),
|
||||
},
|
||||
uptimeItem,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.features.stats.isEnabled) {
|
||||
if (config.features.megaEth.isEnabled) {
|
||||
return uptimeItem;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -296,7 +296,8 @@ export default function useNavItems(): ReturnType {
|
||||
text: 'Charts & stats',
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'stats',
|
||||
isActive: pathname.startsWith('/stats'),
|
||||
isActive: items.some(item => isInternalItem(item) && item.isActive),
|
||||
subItems: items,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -316,11 +317,6 @@ export default function useNavItems(): ReturnType {
|
||||
nextRoute: { pathname: '/contract-verification' as const },
|
||||
isActive: pathname.startsWith('/contract-verification'),
|
||||
},
|
||||
config.features.gasTracker.isEnabled && {
|
||||
text: 'Gas tracker',
|
||||
nextRoute: { pathname: '/gas-tracker' as const },
|
||||
isActive: pathname.startsWith('/gas-tracker'),
|
||||
},
|
||||
config.features.publicTagsSubmission.isEnabled && {
|
||||
text: 'Submit public tag',
|
||||
nextRoute: { pathname: '/public-tags/submit' as const },
|
||||
|
||||
@@ -27,6 +27,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/stats': 'Root page',
|
||||
'/stats/[id]': 'Regular page',
|
||||
'/uptime': 'Root page',
|
||||
'/hot-contracts': 'Root page',
|
||||
'/api-docs': 'Regular page',
|
||||
'/search-results': 'Regular page',
|
||||
'/auth/profile': 'Root page',
|
||||
|
||||
@@ -30,6 +30,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/stats': DEFAULT_TEMPLATE,
|
||||
'/stats/[id]': DEFAULT_TEMPLATE,
|
||||
'/uptime': DEFAULT_TEMPLATE,
|
||||
'/hot-contracts': DEFAULT_TEMPLATE,
|
||||
'/api-docs': DEFAULT_TEMPLATE,
|
||||
'/search-results': DEFAULT_TEMPLATE,
|
||||
'/auth/profile': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -31,6 +31,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/stats': '%network_name% stats - %network_name% network insights',
|
||||
'/stats/[id]': '%network_name% stats - %id% chart',
|
||||
'/uptime': '%network_name% uptime',
|
||||
'/hot-contracts': '%network_name% hot contracts',
|
||||
'/api-docs': '%network_name% API docs - %network_name% developer tools',
|
||||
'/search-results': '%network_name% search result for %q%',
|
||||
'/auth/profile': '%network_name% - my profile',
|
||||
|
||||
@@ -25,6 +25,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/stats': 'Stats',
|
||||
'/stats/[id]': 'Stats chart',
|
||||
'/uptime': 'Uptime',
|
||||
'/hot-contracts': 'Hot contracts',
|
||||
'/api-docs': 'REST API',
|
||||
'/search-results': 'Search results',
|
||||
'/auth/profile': 'Profile',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts';
|
||||
import type { HotContractsResponse, VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts';
|
||||
|
||||
export const contract1: VerifiedContract = {
|
||||
address: {
|
||||
@@ -83,3 +83,34 @@ export const baseResponse: VerifiedContractsResponse = {
|
||||
smart_contract_id: '172',
|
||||
},
|
||||
};
|
||||
|
||||
export const hotContractsResponse: HotContractsResponse = {
|
||||
items: [
|
||||
{
|
||||
contract_address: { ...contract1.address, name: null, reputation: 'scam' },
|
||||
balance: '1000000000000000000',
|
||||
transactions_count: '1000',
|
||||
total_gas_used: '100000000',
|
||||
},
|
||||
{
|
||||
contract_address: {
|
||||
...contract2.address,
|
||||
metadata: {
|
||||
reputation: null,
|
||||
tags: [
|
||||
{ tagType: 'protocol', name: 'Goose', slug: 'goose', ordinal: 1, meta: null },
|
||||
],
|
||||
},
|
||||
},
|
||||
balance: '420',
|
||||
transactions_count: '42',
|
||||
total_gas_used: '12343566',
|
||||
},
|
||||
],
|
||||
next_page_params: {
|
||||
items_count: '50',
|
||||
transactions_count: '50',
|
||||
total_gas_used: '50',
|
||||
contract_address_hash: '50',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -142,6 +142,14 @@ export const gasTracker: Guard = (chainConfig: typeof config) => async() => {
|
||||
}
|
||||
};
|
||||
|
||||
export const hotContracts: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.hotContracts.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const advancedFilter: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.advancedFilter.isEnabled) {
|
||||
return {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const accountsLabelSearch = factory([ guards.accountsLabelSearch ]);
|
||||
export const validators = factory([ guards.validators ]);
|
||||
export const validatorDetails = factory([ guards.validatorDetails ]);
|
||||
export const gasTracker = factory([ guards.gasTracker ]);
|
||||
export const hotContracts = factory([ guards.hotContracts ]);
|
||||
export const advancedFilter = factory([ guards.advancedFilter ]);
|
||||
export const dataAvailability = factory([ guards.dataAvailability ]);
|
||||
export const login = factory([ guards.login ]);
|
||||
|
||||
@@ -59,6 +59,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/epochs">
|
||||
| DynamicRoute<"/essential-dapps/[id]", { "id": string }>
|
||||
| StaticRoute<"/gas-tracker">
|
||||
| StaticRoute<"/hot-contracts">
|
||||
| StaticRoute<"/">
|
||||
| StaticRoute<"/internal-txs">
|
||||
| StaticRoute<"/interop-messages">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const HotContracts = dynamic(() => import('ui/pages/HotContracts'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/hot-contracts">
|
||||
<HotContracts/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { hotContracts as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
@@ -76,6 +76,7 @@
|
||||
| "flame"
|
||||
| "flashblock"
|
||||
| "games"
|
||||
| "gas_slim"
|
||||
| "gas_xl"
|
||||
| "gas"
|
||||
| "gear_slim"
|
||||
@@ -86,6 +87,7 @@
|
||||
| "heart_filled"
|
||||
| "heart_outline"
|
||||
| "hexagon"
|
||||
| "hot-contracts"
|
||||
| "hourglass_slim"
|
||||
| "hourglass"
|
||||
| "info_filled"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as stats from '@blockscout/stats-types';
|
||||
import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract';
|
||||
import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
|
||||
import type { HotContract, VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
|
||||
|
||||
import type { SolidityScanReport } from 'lib/solidityScan/schema';
|
||||
|
||||
@@ -117,3 +117,10 @@ export const MUD_SYSTEMS: SmartContractMudSystemsResponse = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const HOT_CONTRACTS: HotContract = {
|
||||
contract_address: VERIFIED_CONTRACT_INFO.address,
|
||||
balance: '1000000000000000000',
|
||||
transactions_count: '1000',
|
||||
total_gas_used: '100000000',
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface TagProps extends ChakraTag.RootProps {
|
||||
truncated?: boolean;
|
||||
loading?: boolean;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||
@@ -31,6 +32,7 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||
truncated = false,
|
||||
loading,
|
||||
selected,
|
||||
disabled,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -48,7 +50,8 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||
<Skeleton loading={ loading } asChild>
|
||||
<ChakraTag.Root
|
||||
ref={ ref }
|
||||
{ ...(selected && { 'data-selected': true }) }
|
||||
{ ...(selected && !loading && { 'data-selected': true }) }
|
||||
{ ...(disabled && { 'data-disabled': true }) }
|
||||
{ ...rest }
|
||||
>
|
||||
{ startElement && (
|
||||
|
||||
@@ -14,6 +14,11 @@ export const recipe = defineSlotRecipe({
|
||||
_loading: {
|
||||
borderRadius: 'sm',
|
||||
},
|
||||
_disabled: {
|
||||
opacity: 'control.disabled',
|
||||
pointerEvents: 'none',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
lineClamp: '1',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AddressMetadataTagApi } from './addressMetadata';
|
||||
import type { SmartContractProxyType } from './contract';
|
||||
import type { TokenReputation } from './token';
|
||||
|
||||
export interface AddressImplementation {
|
||||
address_hash: string;
|
||||
@@ -61,6 +62,7 @@ export type AddressParamBasic = {
|
||||
} | null;
|
||||
filecoin?: AddressFilecoinParams;
|
||||
proxy_type?: SmartContractProxyType | null;
|
||||
reputation?: TokenReputation;
|
||||
};
|
||||
|
||||
export type AddressParam = UserTags & AddressParamBasic;
|
||||
|
||||
@@ -39,3 +39,35 @@ export type VerifiedContractsCounters = {
|
||||
smart_contracts: string;
|
||||
verified_smart_contracts: string;
|
||||
};
|
||||
|
||||
export interface HotContract {
|
||||
contract_address: AddressParam ;
|
||||
balance: string;
|
||||
transactions_count: string;
|
||||
total_gas_used: string;
|
||||
}
|
||||
|
||||
export interface HotContractsResponse {
|
||||
items: Array<HotContract>;
|
||||
next_page_params: {
|
||||
items_count: string;
|
||||
transactions_count: string;
|
||||
total_gas_used: string;
|
||||
contract_address_hash: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface HotContractsFilters {
|
||||
scale?: HotContractsInterval;
|
||||
}
|
||||
|
||||
export interface HotContractsSorting {
|
||||
sort: 'transactions_count' | 'total_gas_used';
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export type HotContractsSortingField = HotContractsSorting['sort'];
|
||||
|
||||
export type HotContractsSortingValue = `${ HotContractsSortingField }-${ HotContractsSorting['order'] }` | 'default';
|
||||
|
||||
export type HotContractsInterval = '5m' | '1h' | '3h' | '1d' | '7d' | '30d';
|
||||
|
||||
@@ -181,7 +181,7 @@ const Stats = () => {
|
||||
},
|
||||
hasGasTracker && apiData?.gas_prices && {
|
||||
id: 'gas_tracker' as const,
|
||||
icon: 'gas' as const,
|
||||
icon: 'gas_slim' as const,
|
||||
label: 'Gas tracker',
|
||||
value: apiData.gas_prices.average ? <GasPrice data={ apiData.gas_prices.average }/> : 'N/A',
|
||||
hint: gasInfoTooltip,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createListCollection } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { HotContractsInterval } from 'types/api/contracts';
|
||||
|
||||
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
|
||||
import type { SelectOption } from 'toolkit/chakra/select';
|
||||
import { Select } from 'toolkit/chakra/select';
|
||||
import type { TagProps } from 'toolkit/chakra/tag';
|
||||
import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect';
|
||||
|
||||
import { INTERVAL_ITEMS } from './utils';
|
||||
|
||||
const intervalCollection = createListCollection<SelectOption<string>>({
|
||||
items: INTERVAL_ITEMS.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.labelFull,
|
||||
})),
|
||||
});
|
||||
|
||||
const intervalItems = INTERVAL_ITEMS.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.labelShort,
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
interval: HotContractsInterval;
|
||||
onIntervalChange: (newInterval: HotContractsInterval) => void;
|
||||
isLoading?: boolean;
|
||||
selectTagSize?: TagProps['size'];
|
||||
};
|
||||
|
||||
const HotContractsIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagSize }: Props) => {
|
||||
|
||||
const isInitialLoading = useIsInitialLoading(isLoading);
|
||||
|
||||
const handleItemSelect = React.useCallback(({ value }: { value: Array<string> }) => {
|
||||
onIntervalChange(value[0] as HotContractsInterval);
|
||||
}, [ onIntervalChange ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TagGroupSelect<HotContractsInterval>
|
||||
items={ intervalItems }
|
||||
onChange={ onIntervalChange }
|
||||
value={ interval }
|
||||
tagSize={ selectTagSize }
|
||||
loading={ isInitialLoading }
|
||||
disabled={ isLoading }
|
||||
hideBelow="lg"
|
||||
/>
|
||||
<Select
|
||||
collection={ intervalCollection }
|
||||
placeholder="Select interval"
|
||||
value={ [ interval ] }
|
||||
onValueChange={ handleItemSelect }
|
||||
hideFrom="lg"
|
||||
w="100%"
|
||||
loading={ isInitialLoading }
|
||||
disabled={ isLoading }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HotContractsIntervalSelect);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import type { HotContract } from 'types/api/contracts';
|
||||
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import { Reputation } from 'ui/shared/entities/token/TokenEntity';
|
||||
import EntityTags from 'ui/shared/EntityTags/EntityTags';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import NativeCoinValue from 'ui/shared/value/NativeCoinValue';
|
||||
|
||||
interface Props {
|
||||
data: HotContract;
|
||||
isLoading?: boolean;
|
||||
exchangeRate: string | null;
|
||||
}
|
||||
|
||||
const HotContractsListItem = ({ data, isLoading, exchangeRate }: Props) => {
|
||||
const protocolTags = data?.contract_address?.metadata?.tags?.filter(tag => tag.tagType === 'protocol');
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 } py={ 4 } textStyle="sm">
|
||||
<HStack justifyContent="space-between" width="100%">
|
||||
<AddressEntity
|
||||
address={ data.contract_address }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
<Reputation value={ data.contract_address.reputation ?? null }/>
|
||||
</HStack>
|
||||
{ protocolTags && protocolTags.length > 0 && (
|
||||
<EntityTags
|
||||
isLoading={ isLoading }
|
||||
tags={ protocolTags }
|
||||
/>
|
||||
) }
|
||||
<HStack>
|
||||
<Skeleton loading={ isLoading } fontWeight={ 500 } w="100px">Txn count</Skeleton>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span>{ Number(data.transactions_count).toLocaleString() }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Skeleton loading={ isLoading } fontWeight={ 500 } w="100px">Gas used</Skeleton>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span>{ BigNumber(data.total_gas_used || 0).toFormat() }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
<HStack alignItems="flex-start">
|
||||
<Skeleton loading={ isLoading } fontWeight={ 500 } w="100px">Balance</Skeleton>
|
||||
<NativeCoinValue
|
||||
amount={ data.balance }
|
||||
loading={ isLoading }
|
||||
exchangeRate={ exchangeRate }
|
||||
flexWrap="wrap"
|
||||
/>
|
||||
</HStack>
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HotContractsListItem);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { HotContract, HotContractsSortingField, HotContractsSortingValue } from 'types/api/contracts';
|
||||
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
|
||||
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import getNextSortValue from 'ui/shared/sort/getNextSortValue';
|
||||
|
||||
import HotContractsTableItem from './HotContractsTableItem';
|
||||
import { SORT_SEQUENCE } from './utils';
|
||||
|
||||
interface Props {
|
||||
items: Array<HotContract> | undefined;
|
||||
isLoading?: boolean;
|
||||
sort: HotContractsSortingValue;
|
||||
setSorting: ({ value }: { value: Array<string> }) => void;
|
||||
exchangeRate: string | null;
|
||||
};
|
||||
|
||||
const HotContractsTable = ({ items, isLoading, sort, setSorting, exchangeRate }: Props) => {
|
||||
|
||||
const onSortToggle = React.useCallback((field: HotContractsSortingField) => {
|
||||
const value = getNextSortValue<HotContractsSortingField, HotContractsSortingValue>(SORT_SEQUENCE, field)(sort);
|
||||
setSorting({ value: [ value ] });
|
||||
}, [ sort, setSorting ]);
|
||||
|
||||
return (
|
||||
<TableRoot minWidth="900px">
|
||||
<TableHeaderSticky top={ ACTION_BAR_HEIGHT_DESKTOP }>
|
||||
<TableRow>
|
||||
<TableColumnHeader width="25%">Contract</TableColumnHeader>
|
||||
<TableColumnHeaderSortable
|
||||
width="25%"
|
||||
isNumeric
|
||||
sortField="transactions_count"
|
||||
sortValue={ sort }
|
||||
onSortToggle={ onSortToggle }
|
||||
disabled={ isLoading }
|
||||
>
|
||||
Txn count
|
||||
</TableColumnHeaderSortable>
|
||||
<TableColumnHeaderSortable
|
||||
width="25%"
|
||||
isNumeric
|
||||
sortField="total_gas_used"
|
||||
sortValue={ sort }
|
||||
onSortToggle={ onSortToggle }
|
||||
disabled={ isLoading }
|
||||
>
|
||||
Gas used
|
||||
</TableColumnHeaderSortable>
|
||||
<TableColumnHeader width="25%" isNumeric>Balance { currencyUnits.ether }</TableColumnHeader>
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ items?.map((item, index) => (
|
||||
<HotContractsTableItem key={ index } isLoading={ isLoading } data={ item } exchangeRate={ exchangeRate }/>
|
||||
)) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotContractsTable;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import type { HotContract } from 'types/api/contracts';
|
||||
|
||||
import { TableCell, TableRow } from 'toolkit/chakra/table';
|
||||
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import { Reputation } from 'ui/shared/entities/token/TokenEntity';
|
||||
import EntityTags from 'ui/shared/EntityTags/EntityTags';
|
||||
import NativeCoinValue from 'ui/shared/value/NativeCoinValue';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
data: HotContract;
|
||||
exchangeRate: string | null;
|
||||
};
|
||||
|
||||
const HotContractsTableItem = ({
|
||||
isLoading,
|
||||
data,
|
||||
exchangeRate,
|
||||
}: Props) => {
|
||||
const protocolTags = data?.contract_address?.metadata?.tags?.filter(tag => tag.tagType === 'protocol');
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell verticalAlign="middle">
|
||||
<HStack>
|
||||
<AddressEntity
|
||||
address={ data.contract_address }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
<Reputation value={ data.contract_address.reputation ?? null } ml={ 0 }/>
|
||||
</HStack>
|
||||
{ protocolTags && protocolTags.length > 0 && (
|
||||
<EntityTags
|
||||
isLoading={ isLoading }
|
||||
tags={ protocolTags }
|
||||
mt="10px"
|
||||
/>
|
||||
) }
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<TruncatedText text={ Number(data.transactions_count).toLocaleString() } loading={ isLoading } maxW="100%"/>
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<TruncatedText text={ BigNumber(data.total_gas_used || 0).toFormat() } loading={ isLoading } maxW="100%"/>
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<NativeCoinValue
|
||||
amount={ data.balance }
|
||||
loading={ isLoading }
|
||||
exchangeRate={ exchangeRate }
|
||||
layout="vertical"
|
||||
rowGap="10px"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotContractsTableItem;
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { HotContractsSortingValue, HotContractsSortingField, HotContractsInterval } from 'types/api/contracts';
|
||||
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import type { SelectOption } from 'toolkit/chakra/select';
|
||||
|
||||
export const SORT_OPTIONS: Array<SelectOption<HotContractsSortingValue>> = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Txs count descending', value: 'transactions_count-desc' },
|
||||
{ label: 'Txs count ascending', value: 'transactions_count-asc' },
|
||||
{ label: 'Gas used descending', value: 'total_gas_used-desc' },
|
||||
{ label: 'Gas used ascending', value: 'total_gas_used-asc' },
|
||||
];
|
||||
|
||||
export const SORT_SEQUENCE: Record<HotContractsSortingField, Array<HotContractsSortingValue>> = {
|
||||
transactions_count: [ 'transactions_count-desc', 'transactions_count-asc', 'default' ],
|
||||
total_gas_used: [ 'total_gas_used-desc', 'total_gas_used-asc', 'default' ],
|
||||
};
|
||||
|
||||
export const INTERVAL_ITEMS: Array<{ id: HotContractsInterval; labelShort: string; labelFull: string }> = [
|
||||
{ id: '5m', labelShort: '5m', labelFull: '5 minutes' },
|
||||
{ id: '1h', labelShort: '1h', labelFull: '1 hour' },
|
||||
{ id: '3h', labelShort: '3h', labelFull: '3 hours' },
|
||||
{ id: '1d', labelShort: '1D', labelFull: '1 day' },
|
||||
{ id: '7d', labelShort: '1W', labelFull: '1 week' },
|
||||
{ id: '30d', labelShort: '1M', labelFull: '1 month' },
|
||||
];
|
||||
|
||||
export const getIntervalValueFromQuery = (query: string | Array<string> | undefined): HotContractsInterval => {
|
||||
const queryString = getQueryParamString(query);
|
||||
if (queryString) {
|
||||
const interval = INTERVAL_ITEMS.find(item => item.id === queryString);
|
||||
if (interval) {
|
||||
return interval.id;
|
||||
}
|
||||
}
|
||||
|
||||
return INTERVAL_ITEMS[0].id;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as contractsMock from 'mocks/contracts/index';
|
||||
import * as statsMock from 'mocks/stats/index';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
import { getIntervalValueFromQuery } from 'ui/hotContracts/utils';
|
||||
|
||||
import HotContracts from './HotContracts';
|
||||
|
||||
test('base view +@mobile', async({ render, mockTextAd, mockApiResponse, mockEnvs }) => {
|
||||
test.slow();
|
||||
await mockEnvs([ [ 'NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED', 'true' ] ]);
|
||||
await mockTextAd();
|
||||
await mockApiResponse(
|
||||
'general:stats_hot_contracts',
|
||||
contractsMock.hotContractsResponse,
|
||||
{ queryParams: { scale: getIntervalValueFromQuery(undefined) } },
|
||||
);
|
||||
await mockApiResponse('general:stats', { ...statsMock.base, coin_price: '3214.42' });
|
||||
|
||||
const component = await render(<HotContracts/>);
|
||||
await expect(component).toHaveScreenshot({ timeout: 10_000 });
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Box, createListCollection, Flex } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { HotContractsInterval, HotContractsSorting, HotContractsSortingField, HotContractsSortingValue } from 'types/api/contracts';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { HOT_CONTRACTS } from 'stubs/contract';
|
||||
import { HOMEPAGE_STATS } from 'stubs/stats';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { apos } from 'toolkit/utils/htmlEntities';
|
||||
import HotContractsIntervalSelect from 'ui/hotContracts/HotContractsIntervalSelect';
|
||||
import HotContractsListItem from 'ui/hotContracts/HotContractsListItem';
|
||||
import HotContractsTable from 'ui/hotContracts/HotContractsTable';
|
||||
import { getIntervalValueFromQuery, SORT_OPTIONS } from 'ui/hotContracts/utils';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
|
||||
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
|
||||
import Sort from 'ui/shared/sort/Sort';
|
||||
|
||||
const sortCollection = createListCollection({
|
||||
items: SORT_OPTIONS,
|
||||
});
|
||||
|
||||
const HotContracts = () => {
|
||||
const router = useRouter();
|
||||
const [ interval, setInterval ] = React.useState<HotContractsInterval>(getIntervalValueFromQuery(router.query.scale));
|
||||
const [ sort, setSort ] =
|
||||
React.useState<HotContractsSortingValue>(getSortValueFromQuery<HotContractsSortingValue>(router.query, SORT_OPTIONS) ?? 'default');
|
||||
|
||||
const { data, isError, isPlaceholderData, pagination, onSortingChange, onFilterChange } = useQueryWithPages({
|
||||
resourceName: 'general:stats_hot_contracts',
|
||||
filters: { scale: interval },
|
||||
sorting: getSortParamsFromValue<HotContractsSortingValue, HotContractsSortingField, HotContractsSorting['order']>(sort),
|
||||
options: {
|
||||
placeholderData: {
|
||||
items: Array(50).fill(HOT_CONTRACTS),
|
||||
next_page_params: { items_count: '50', transactions_count: '50', total_gas_used: '50', contract_address_hash: '50' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statsQuery = useApiQuery('general:stats', {
|
||||
queryOptions: {
|
||||
placeholderData: HOMEPAGE_STATS,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSortChange = React.useCallback(({ value }: { value: Array<string> }) => {
|
||||
setSort(value[0] as HotContractsSortingValue);
|
||||
onSortingChange(value[0] === 'default' ? undefined : getSortParamsFromValue(value[0] as HotContractsSortingValue));
|
||||
}, [ onSortingChange ]);
|
||||
|
||||
const handleIntervalChange = React.useCallback((newInterval: HotContractsInterval) => {
|
||||
setInterval(newInterval);
|
||||
onFilterChange({ scale: newInterval });
|
||||
}, [ onFilterChange ]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Box hideFrom="lg">
|
||||
{ data?.items.map((item, index) => (
|
||||
<HotContractsListItem
|
||||
key={ item.contract_address.hash + (isPlaceholderData ? index : '') }
|
||||
isLoading={ isPlaceholderData }
|
||||
data={ item }
|
||||
exchangeRate={ statsQuery.data?.coin_price ?? null }
|
||||
/>
|
||||
)) }
|
||||
</Box>
|
||||
<Box hideBelow="lg">
|
||||
<HotContractsTable
|
||||
items={ data?.items }
|
||||
isLoading={ isPlaceholderData }
|
||||
sort={ sort }
|
||||
setSorting={ handleSortChange }
|
||||
exchangeRate={ statsQuery.data?.coin_price ?? null }
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
const actionBar = (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Flex alignItems="center">
|
||||
<HotContractsIntervalSelect
|
||||
interval={ interval }
|
||||
onIntervalChange={ handleIntervalChange }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
{ [ '1d', '7d', '30d' ].includes(interval) && (
|
||||
<Skeleton loading={ isPlaceholderData } color="text.secondary" hideBelow="lg" textStyle="sm" ml={ 6 }>
|
||||
<span>The data is updated once a day.</span>
|
||||
</Skeleton>
|
||||
) }
|
||||
<Sort
|
||||
name="hot_contracts_sorting"
|
||||
defaultValue={ [ sort ] }
|
||||
collection={ sortCollection }
|
||||
onValueChange={ handleSortChange }
|
||||
isLoading={ isPlaceholderData }
|
||||
hideFrom="lg"
|
||||
ml={ 2 }
|
||||
/>
|
||||
</Flex>
|
||||
<Pagination { ...pagination } ml="auto"/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title="Hot contracts"
|
||||
withTextAd
|
||||
/>
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
itemsNum={ data?.items.length }
|
||||
emptyText="There are no hot contracts."
|
||||
actionBar={ actionBar }
|
||||
filterProps={{
|
||||
emptyFilteredText: `Couldn${ apos }t find hot contracts that matches your filter query.`,
|
||||
hasActiveFilters: Boolean(interval),
|
||||
}}
|
||||
>
|
||||
{ content }
|
||||
</DataListDisplay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotContracts;
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -1,6 +1,7 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
import type { SelectRootProps } from 'toolkit/chakra/select';
|
||||
@@ -14,16 +15,18 @@ export interface Props extends SelectRootProps {
|
||||
const Sort = (props: Props) => {
|
||||
const { collection, isLoading, ...rest } = props;
|
||||
const isMobile = useIsMobile(false);
|
||||
const isInitialLoading = useIsInitialLoading(isLoading);
|
||||
|
||||
const trigger = (() => {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<SelectControl triggerProps={{ asChild: true }} noIndicator>
|
||||
<IconButton
|
||||
loadingSkeleton={ isLoading }
|
||||
loadingSkeleton={ isInitialLoading }
|
||||
aria-label="sort"
|
||||
size="md"
|
||||
variant="dropdown"
|
||||
disabled={ isLoading }
|
||||
>
|
||||
<IconSvg name="arrows/up-down"/>
|
||||
</IconButton>
|
||||
@@ -33,7 +36,7 @@ const Sort = (props: Props) => {
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
loading={ isLoading }
|
||||
loading={ isInitialLoading }
|
||||
_hover={{ color: 'hover' }}
|
||||
_open={{ color: 'hover' }}
|
||||
>
|
||||
@@ -56,7 +59,7 @@ const Sort = (props: Props) => {
|
||||
})();
|
||||
|
||||
return (
|
||||
<SelectRoot collection={ collection } w="fit-content" variant="plain" { ...rest }>
|
||||
<SelectRoot collection={ collection } w="fit-content" variant="plain" disabled={ isLoading } { ...rest }>
|
||||
{ trigger }
|
||||
<SelectContent>
|
||||
{ collection.items.map((item) => (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { StackProps } from '@chakra-ui/react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
@@ -7,6 +8,8 @@ import { Tag } from 'toolkit/chakra/tag';
|
||||
type Props<T extends string> = {
|
||||
items: Array<{ id: T; title: string }>;
|
||||
tagSize?: TagProps['size'];
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
} & (
|
||||
{
|
||||
value?: T;
|
||||
@@ -17,9 +20,9 @@ type Props<T extends string> = {
|
||||
onChange: (value: Array<T>) => void;
|
||||
isMulti: true;
|
||||
}
|
||||
);
|
||||
) & Omit<StackProps, 'onChange'>;
|
||||
|
||||
const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tagSize, ...rest }: Props<T>) => {
|
||||
const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tagSize, loading, disabled, ...rest }: Props<T>) => {
|
||||
const onItemClick = React.useCallback((event: React.SyntheticEvent) => {
|
||||
const itemValue = (event.currentTarget as HTMLDivElement).getAttribute('data-id') as T;
|
||||
if (isMulti) {
|
||||
@@ -46,10 +49,12 @@ const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tag
|
||||
data-id={ item.id }
|
||||
selected={ isSelected }
|
||||
fontWeight={ 500 }
|
||||
onClick={ onItemClick }
|
||||
onClick={ disabled ? undefined : onItemClick }
|
||||
size={ tagSize }
|
||||
display="inline-flex"
|
||||
justifyContent="center"
|
||||
loading={ loading }
|
||||
disabled={ disabled }
|
||||
>
|
||||
{ item.title }
|
||||
</Tag>
|
||||
|
||||
@@ -65,6 +65,10 @@ const TagShowcase = () => {
|
||||
<Tag loading>My tag</Tag>
|
||||
<Tag maxW="150px" truncated loading>Very very very very very looooooonggggg text</Tag>
|
||||
</Sample>
|
||||
<Sample label="loading: true, variant: select">
|
||||
<Tag variant="select" loading>Default</Tag>
|
||||
<Tag variant="select" selected loading>Selected</Tag>
|
||||
</Sample>
|
||||
<Sample label="loading: false">
|
||||
<Tag>My tag</Tag>
|
||||
<Tag maxW="150px" truncated>Very very very very very looooooonggggg text</Tag>
|
||||
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |