Commit 7141aa3c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-2029

parents a67f0714 dc83febb
......@@ -37,6 +37,7 @@ import type {
AddressMudRecordsFilter,
AddressMudRecordsSorting,
AddressMudRecord,
AddressEpochRewardsResponse,
} from 'types/api/address';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
......@@ -522,6 +523,11 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ],
filterFields: [],
},
address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards',
pathParams: [ 'hash' as const ],
filterFields: [],
},
// CONTRACT
contract: {
......@@ -1019,7 +1025,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'addresses' | 'addresses_metadata_search' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_epoch_rewards' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
......@@ -1199,6 +1205,7 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
never;
......
import type { AddressEpochRewardsResponse } from 'types/api/address';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import { withEns, withName, withoutName } from './address';
export const epochRewards: AddressEpochRewardsResponse = {
items: [
{
type: 'delegated_payment',
amount: '136609473658452408568',
account: withName,
associated_account: withName,
block_hash: '0x',
block_number: 26369280,
epoch_number: 1526,
token: tokenInfo,
},
{
type: 'group',
amount: '117205842355246195095',
account: withoutName,
associated_account: withoutName,
block_hash: '0x',
block_number: 26352000,
epoch_number: 1525,
token: tokenInfo,
},
{
type: 'validator',
amount: '125659647325556554060',
account: withEns,
associated_account: withEns,
block_hash: '0x',
block_number: 26300160,
epoch_number: 1524,
token: tokenInfo,
},
],
next_page_params: null,
};
......@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = {
data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>',
},
};
export const noteTag2: AddressMetadataTagApi = {
slug: 'note0',
name: 'note_0',
tagType: 'note',
ordinal: 0,
meta: {
alertStatus: 'info',
data: 'The token MILF was launched on May 13, 2021. The maximum total supply of the token is 100 billion.',
},
};
......@@ -3,6 +3,7 @@ import type {
AddressCoinBalanceHistoryItem,
AddressCollection,
AddressCounters,
AddressEpochRewardsItem,
AddressMudTableItem,
AddressNFT,
AddressTabsCounters,
......@@ -10,7 +11,7 @@ import type {
} from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { MUD_SCHEMA, MUD_TABLE } from './mud';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx';
......@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
schema: MUD_SCHEMA,
table: MUD_TABLE,
};
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
amount: '136609473658452408568',
block_number: 10355938,
type: 'voter',
token: TOKEN_INFO_ERC_20,
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
account: ADDRESS_PARAMS,
epoch_number: 1526,
associated_account: ADDRESS_PARAMS,
};
......@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => {
// for buttons in the hero banner
const variantHero = defineStyle((props) => {
const buttonConfig = config.UI.homepage.heroBanner?.button;
return {
bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600',
buttonConfig?._default?.background?.[0] || 'blue.600',
buttonConfig?._default?.background?.[1] || buttonConfig?._default?.background?.[0] || 'blue.600',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white',
buttonConfig?._default?.text_color?.[0] || 'white',
buttonConfig?._default?.text_color?.[1] || buttonConfig?._default?.text_color?.[0] || 'white',
)(props),
_hover: {
bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400',
buttonConfig?._hover?.background?.[0] || 'blue.400',
buttonConfig?._hover?.background?.[1] || buttonConfig?._hover?.background?.[0] || 'blue.400',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white',
buttonConfig?._hover?.text_color?.[0] || 'white',
buttonConfig?._hover?.text_color?.[1] || buttonConfig?._hover?.text_color?.[0] || 'white',
)(props),
},
'&[data-selected=true]': {
bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50',
buttonConfig?._selected?.background?.[0] || 'blue.50',
buttonConfig?._selected?.background?.[1] || buttonConfig?._selected?.background?.[0] || 'blue.50',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800',
buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
buttonConfig?._selected?.text_color?.[1] || buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
)(props),
},
};
......
import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation } from './addressParams';
import type { Block } from './block';
import type { UserTags, AddressImplementation, AddressParam } from './addressParams';
import type { Block, EpochRewardsType } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
......@@ -191,6 +191,7 @@ export type AddressTabsCounters = {
transactions_count: number | null;
validations_count: number | null;
withdrawals_count: number | null;
celo_election_rewards_count?: number | null;
}
// MUD framework
......@@ -245,3 +246,25 @@ export type AddressMudRecord = {
schema: MudWorldSchema;
table: MudWorldTable;
}
export type AddressEpochRewardsResponse = {
items: Array<AddressEpochRewardsItem>;
next_page_params: {
amount: string;
associated_account_address_hash: string;
block_number: number;
items_count: number;
type: EpochRewardsType;
} | null;
}
export type AddressEpochRewardsItem = {
type: EpochRewardsType;
token: TokenInfo;
amount: string;
block_number: number;
block_hash: string;
account: AddressParam;
epoch_number: number;
associated_account: AddressParam;
}
......@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward {
total: string;
}
export type EpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
export interface BlockEpoch {
number: number;
distribution: {
......@@ -151,12 +153,7 @@ export interface BlockEpoch {
community_transfer: TokenTransfer | null;
reserve_bolster_transfer: TokenTransfer | null;
};
aggregated_election_rewards: {
delegated_payment: BlockEpochElectionReward | null;
group: BlockEpochElectionReward | null;
validator: BlockEpochElectionReward | null;
voter: BlockEpochElectionReward | null;
};
aggregated_election_rewards: Record<EpochRewardsType, BlockEpochElectionReward | null>;
}
export interface BlockEpochElectionRewardDetails {
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { epochRewards } from 'mocks/address/epochRewards';
import { test, expect } from 'playwright/lib';
import AddressEpochRewards from './AddressEpochRewards';
const ADDRESS_HASH = '0x1234';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressEpochRewards/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import { EPOCH_REWARD_ITEM } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressEpochRewardsTable from 'ui/address/epochRewards/AddressEpochRewardsTable';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
}
const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash);
const rewardsQuery = useQueryWithPages({
resourceName: 'address_epoch_rewards',
pathParams: {
hash,
},
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
amount: '1',
items_count: 50,
type: 'voter',
associated_account_address_hash: '1',
block_number: 10355938,
} }),
},
});
if (!isMounted || !shouldRender) {
return null;
}
const content = rewardsQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressEpochRewardsTable
items={ rewardsQuery.data.items }
top={ rewardsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ rewardsQuery.isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ rewardsQuery.data.items.map((item, index) => (
<AddressEpochRewardsListItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ rewardsQuery.isPlaceholderData }
/>
)) }
</Show>
</>
) : null;
const actionBar = rewardsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...rewardsQuery.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ rewardsQuery.isError }
items={ rewardsQuery.data?.items }
emptyText="There are no epoch rewards for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressEpochRewards;
import { Flex, Skeleton } from '@chakra-ui/react';
import { Text, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex>
<Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
</Skeleton>
<BlockGasUsed
gasUsed={ props.gas_used }
gasLimit={ props.gas_limit }
......@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ totalReward.toFixed() }</Text>
</Skeleton>
</Flex>
) }
</ListItemMobile>
......
......@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
</Flex>
</Td>
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Td isNumeric display="flex" justifyContent="end">
<Td isNumeric>
<Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
......
......@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import AddressMetadataAlert from './AddressMetadataAlert';
test('base view', async({ render }) => {
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag ] }/>);
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag, metadataMock.noteTag2 ] }/>);
await expect(component).toHaveScreenshot();
});
import { Alert, chakra } from '@chakra-ui/react';
import { Alert, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
......@@ -9,20 +9,17 @@ interface Props {
}
const AddressMetadataAlert = ({ tags, className }: Props) => {
const noteTag = tags?.find(({ tagType }) => tagType === 'note');
if (!noteTag) {
return null;
}
const content = noteTag.meta?.data;
const noteTags = tags?.filter(({ tagType }) => tagType === 'note').filter(({ meta }) => meta?.data);
if (!content) {
if (!noteTags?.length) {
return null;
}
return (
<Flex flexDir="column" gap={ 3 } className={ className }>
{ noteTags.map((noteTag) => (
<Alert
className={ className }
key={ noteTag.name }
status={ noteTag.meta?.alertStatus ?? 'error' }
bgColor={ noteTag.meta?.alertBgColor }
color={ noteTag.meta?.alertTextColor }
......@@ -36,8 +33,10 @@ const AddressMetadataAlert = ({ tags, className }: Props) => {
},
},
}}
dangerouslySetInnerHTML={{ __html: content }}
dangerouslySetInnerHTML={{ __html: noteTag.meta?.data ?? '' }}
/>
)) }
</Flex>
);
};
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, accuracy: 2, decimals: item.token.decimals });
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ Number(item.block_number) }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Epoch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.epoch_number }
</ListItemMobileGrid.Value>
{ /* <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
display="inline-block"
/>
</ListItemMobileGrid.Value> */ }
<ListItemMobileGrid.Label isLoading={ isLoading }>Reward type</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Associated address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={ item.associated_account }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 }>
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default AddressEpochRewardsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem';
type Props = {
items: Array<AddressEpochRewardsItem>;
isLoading?: boolean;
top: number;
};
const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
return (
<Table variant="simple" size="sm" minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Block</Th>
<Th>Reward type</Th>
<Th>Associated address</Th>
<Th isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => {
return (
<AddressEpochRewardsTableItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
);
}) }
</Tbody>
</Table>
);
};
export default AddressEpochRewardsTable;
import { Flex, Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, decimals: item.token.decimals });
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center" gap={ 3 }>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
<Text color="text_secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text>
{ /* no timestamp from API, will be added later */ }
{ /* <TimeAgoWithTooltip timestamp={ item } isLoading={ isLoading }/> */ }
</Flex>
</Td>
<Td verticalAlign="middle">
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<AddressEntity address={ item.associated_account } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 } justifyContent="flex-end">
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</Td>
</Tr>
);
};
export default AddressEpochRewardsTableItem;
import React from 'react';
import type { BlockEpoch } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
interface Props {
type: keyof BlockEpoch['aggregated_election_rewards'];
isLoading?: boolean;
}
const BlockEpochElectionRewardType = ({ type, isLoading }: Props) => {
switch (type) {
case 'delegated_payment':
return <Tag colorScheme="blue" isLoading={ isLoading }>Delegated payments</Tag>;
case 'group':
return <Tag colorScheme="teal" isLoading={ isLoading }>Validator group rewards</Tag>;
case 'validator':
return <Tag colorScheme="purple" isLoading={ isLoading }>Validator rewards</Tag>;
case 'voter':
return <Tag colorScheme="yellow" isLoading={ isLoading }>Voting rewards</Tag>;
}
};
export default React.memo(BlockEpochElectionRewardType);
......@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
interface Props {
data: BlockEpochElectionReward;
......@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
/>
</Skeleton>
) : <Box boxSize={ 6 }/> }
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton>
<Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
......
......@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
import { getRewardNumText } from './utils';
interface Props {
......@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
) }
</Td>
<Td borderColor={ mainRowBorderColor }>
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
</Td>
<Td borderColor={ mainRowBorderColor }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }>
......
......@@ -13,18 +13,32 @@ const BORDER_DEFAULT = 'none';
const HeroBanner = () => {
const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
// light mode
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.background?.[1] ||
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
);
const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
// light mode
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.text_color?.[1] ||
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
);
const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
);
return (
......
......@@ -24,6 +24,7 @@ import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
import AddressDetails from 'ui/address/AddressDetails';
import AddressEpochRewards from 'ui/address/AddressEpochRewards';
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressLogs from 'ui/address/AddressLogs';
import AddressMud from 'ui/address/AddressMud';
......@@ -195,6 +196,12 @@ const AddressPageContent = () => {
count: addressTabsCountersQuery.data?.internal_txs_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
},
addressTabsCountersQuery.data?.celo_election_rewards_count ? {
id: 'epoch_rewards',
title: 'Epoch rewards',
count: addressTabsCountersQuery.data?.celo_election_rewards_count,
component: <AddressEpochRewards scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} : undefined,
{
id: 'coin_balance_history',
title: 'Coin balance history',
......@@ -282,7 +289,7 @@ const AddressPageContent = () => {
{ slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } :
undefined,
...formatUserTags(addressQuery.data),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags.filter(tag => tag.tagType !== 'note') || []),
].filter(Boolean).sort(sortEntityTags);
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]);
......
......@@ -110,7 +110,10 @@ const Chart = () => {
router.push({
pathname: router.pathname,
query: { ...router.query, resolution },
});
},
undefined,
{ shallow: true },
);
}, [ setResolution, router ]);
const handleReset = React.useCallback(() => {
......
......@@ -45,7 +45,7 @@ const EntityTags = ({ tags, className, isLoading }: Props) => {
+{ tags.length - visibleNum }
</Tag>
</PopoverTrigger>
<PopoverContent w="300px">
<PopoverContent maxW="300px" w="auto">
<PopoverBody >
<Flex columnGap={ 2 } rowGap={ 2 } flexWrap="wrap">
{ tags.slice(visibleNum).map((tag) => <EntityTag key={ tag.slug } data={ tag }/>) }
......
import type { EntityTag } from './types';
import { route } from 'nextjs-routes';
// import { route } from 'nextjs-routes';
export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined {
if (data.meta?.warpcastHandle) {
......@@ -17,10 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna
};
}
if (data.tagType === 'generic' || data.tagType === 'protocol') {
return {
type: 'internal',
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
};
}
// if (data.tagType === 'generic' || data.tagType === 'protocol') {
// return {
// type: 'internal',
// href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
// };
// }
}
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { EpochRewardsType } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
type Props = {
type: EpochRewardsType;
isLoading?: boolean;
};
const TYPE_TAGS: Record<EpochRewardsType, { text: string; label: string; color: string }> = {
group: {
text: 'Validator group rewards',
// eslint-disable-next-line max-len
label: 'Reward given to a validator group. The address being viewed is the group\'s address; the associated address is the validator\'s address on whose behalf the reward was paid.',
color: 'teal',
},
validator: {
text: 'Validator rewards',
label: 'Reward given to a validator. The address being viewed is the validator\'s address; the associated address is the validator group\'s address.',
color: 'purple',
},
delegated_payment: {
text: 'Delegated payments',
// eslint-disable-next-line max-len
label: 'Reward portion delegated by a validator to another address. The address being viewed is the beneficiary receiving the reward; the associated address is the validator who set the delegation.',
color: 'blue',
},
voter: {
text: 'Voting rewards',
label: 'Reward given to a voter. The address being viewed is the voter\'s address; the associated address is the group address.',
color: 'yellow',
},
};
const EpochRewardTypeTag = ({ type, isLoading }: Props) => {
const { text, label, color } = TYPE_TAGS[type];
return (
<Tooltip label={ label } maxW="322px" textAlign="center">
<Tag colorScheme={ color } isLoading={ isLoading }>
{ text }
</Tag>
</Tooltip>
);
};
export default React.memo(EpochRewardTypeTag);
......@@ -106,7 +106,7 @@ const SearchBar = ({ isHomepage }: Props) => {
React.useEffect(() => {
handleSearchTermChange('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ router.pathname ]);
}, [ router.asPath?.split('?')?.[0] ]);
React.useEffect(() => {
const inputEl = inputRef.current;
......
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