Skip to content

Commit

Permalink
Public tags: dedicated tag page (#2217)
Browse files Browse the repository at this point in the history
* page layout

* text with results number

* fix ts

* tests

* add quotes to network id env value

* fix null balance and result num

* fix mobile layout
  • Loading branch information
tom2drum authored Sep 11, 2024
1 parent cac3f63 commit a3cdf62
Show file tree
Hide file tree
Showing 23 changed files with 380 additions and 19 deletions.
2 changes: 1 addition & 1 deletion deploy/values/review/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ imagePullSecrets:
- name: regcred
config:
network:
id: 11155111
id: "11155111"
name: Blockscout
shortname: Blockscout
currency:
Expand Down
10 changes: 8 additions & 2 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type {
AddressMudRecordsSorting,
AddressMudRecord,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type {
ArbitrumL2MessagesResponse,
Expand Down Expand Up @@ -421,6 +421,10 @@ export const RESOURCES = {
path: '/api/v2/addresses/',
filterFields: [ ],
},
addresses_metadata_search: {
path: '/api/v2/proxy/metadata/addresses',
filterFields: [ 'slug' as const, 'tag_type' as const ],
},

// ADDRESS
address: {
Expand Down Expand Up @@ -979,7 +983,7 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_rewards' |
'txs_validated' | 'txs_pending' | 'txs_with_blobs' | 'txs_watchlist' | 'txs_execution_node' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_blobs' |
'addresses' |
'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' |
Expand Down Expand Up @@ -1053,6 +1057,7 @@ Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'tx_blobs' ? TxBlobs :
Q extends 'tx_interpretation' ? TxInterpretationResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'addresses_metadata_search' ? AddressesMetadataSearchResult :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_tabs_counters' ? AddressTabsCounters :
Expand Down Expand Up @@ -1178,6 +1183,7 @@ Q extends 'txs_with_blobs' ? TTxsWithBlobsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'addresses_metadata_search' ? AddressesMetadataSearchFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'address_nfts' ? AddressNFTTokensFilter :
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/getPageOgType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/block/countdown': 'Regular page',
'/block/countdown/[height]': 'Regular page',
'/accounts': 'Root page',
'/accounts/label/[slug]': 'Root page',
'/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page',
'/contract-verification': 'Root page',
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/block/countdown': DEFAULT_TEMPLATE,
'/block/countdown/[height]': DEFAULT_TEMPLATE,
'/accounts': DEFAULT_TEMPLATE,
'/accounts/label/[slug]': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/verified-contracts': DEFAULT_TEMPLATE,
'/contract-verification': DEFAULT_TEMPLATE,
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/block/countdown': '%network_name% block countdown index',
'/block/countdown/[height]': '%network_name% block %height% countdown',
'/accounts': '%network_name% top accounts',
'/accounts/label/[slug]': '%network_name% addresses search by label',
'/address/[hash]': '%network_name% address details for %hash%',
'/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer',
'/contract-verification': '%network_name% verify contract',
Expand Down
1 change: 1 addition & 0 deletions lib/mixpanel/getPageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/block/countdown': 'Block countdown search',
'/block/countdown/[height]': 'Block countdown',
'/accounts': 'Top accounts',
'/accounts/label/[slug]': 'Addresses search by label',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
'/contract-verification': 'Contract verification',
Expand Down
10 changes: 10 additions & 0 deletions nextjs/getServerSideProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ export const accounts: GetServerSideProps<Props> = async(context) => {
return base(context);
};

export const accountsLabelSearch: GetServerSideProps<Props> = async(context) => {
if (!config.features.addressMetadata.isEnabled || !context.query.tagType) {
return {
notFound: true,
};
}

return base(context);
};

export const userOps: GetServerSideProps<Props> = async(context) => {
if (!config.features.userOps.isEnabled) {
return {
Expand Down
1 change: 1 addition & 0 deletions nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist">
| StaticRoute<"/accounts">
| DynamicRoute<"/accounts/label/[slug]", { "slug": string }>
| DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/config">
Expand Down
File renamed without changes.
19 changes: 19 additions & 0 deletions pages/accounts/label/[slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';

import PageNextJs from 'nextjs/PageNextJs';

const AccountsLabelSearch = dynamic(() => import('ui/pages/AccountsLabelSearch'), { ssr: false });

const Page: NextPage = () => {
return (
<PageNextJs pathname="/accounts/label/[slug]">
<AccountsLabelSearch/>
</PageNextJs>
);
};

export default Page;

export { accountsLabelSearch as getServerSideProps } from 'nextjs/getServerSideProps';
12 changes: 11 additions & 1 deletion types/api/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AddressParam } from './addressParams';

export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string }
export type AddressesItem = AddressParam & { tx_count: string; coin_balance: string | null }

export type AddressesResponse = {
items: Array<AddressesItem>;
Expand All @@ -11,3 +11,13 @@ export type AddressesResponse = {
} | null;
total_supply: string;
}

export interface AddressesMetadataSearchResult {
items: Array<AddressesItem>;
next_page_params: null;
}

export interface AddressesMetadataSearchFilters {
slug: string;
tag_type: string;
}
2 changes: 1 addition & 1 deletion ui/addresses/AddressesListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const AddressesListItem = ({
isLoading,
}: Props) => {

const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));

return (
<ListItemMobile rowGap={ 3 }>
Expand Down
2 changes: 1 addition & 1 deletion ui/addresses/AddressesTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AddressesTableItem = ({
isLoading,
}: Props) => {

const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');

return (
Expand Down
48 changes: 48 additions & 0 deletions ui/addressesLabelSearch/AddressesLabelSearchListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';

import type { AddressesItem } from 'types/api/addresses';

import config from 'configs/app';
import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';

type Props = {
item: AddressesItem;
isLoading?: boolean;
}

const AddressesLabelSearchListItem = ({
item,
isLoading,
}: Props) => {

const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));

return (
<ListItemMobile rowGap={ 3 }>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
w="100%"
/>
<HStack spacing={ 3 } maxW="100%" alignItems="flex-start">
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 } flexShrink={ 0 }>{ `Balance ${ currencyUnits.ether }` }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW="0" whiteSpace="pre-wrap">
<span>{ addressBalance.dp(8).toFormat() }</span>
</Skeleton>
</HStack>
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Txn count</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ Number(item.tx_count).toLocaleString() }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
};

export default React.memo(AddressesLabelSearchListItem);
40 changes: 40 additions & 0 deletions ui/addressesLabelSearch/AddressesLabelSearchTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';

import type { AddressesItem } from 'types/api/addresses';

import { currencyUnits } from 'lib/units';
import { default as Thead } from 'ui/shared/TheadSticky';

import AddressesLabelSearchTableItem from './AddressesLabelSearchTableItem';

interface Props {
items: Array<AddressesItem>;
top: number;
isLoading?: boolean;
}

const AddressesLabelSearchTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="70%">Address</Th>
<Th width="15%" isNumeric>{ `Balance ${ currencyUnits.ether }` }</Th>
<Th width="15%" isNumeric>Txn count</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<AddressesLabelSearchTableItem
key={ item.hash + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};

export default AddressesLabelSearchTable;
48 changes: 48 additions & 0 deletions ui/addressesLabelSearch/AddressesLabelSearchTableItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Tr, Td, Text, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';

import type { AddressesItem } from 'types/api/addresses';

import config from 'configs/app';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';

type Props = {
item: AddressesItem;
isLoading?: boolean;
}

const AddressesLabelSearchTableItem = ({
item,
isLoading,
}: Props) => {

const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');

return (
<Tr>
<Td>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
my="2px"
/>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" maxW="100%">
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] + (addressBalanceChunks[1] ? '.' : '') }</Text>
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" lineHeight="24px">
{ Number(item.tx_count).toLocaleString() }
</Skeleton>
</Td>
</Tr>
);
};

export default React.memo(AddressesLabelSearchTableItem);
61 changes: 61 additions & 0 deletions ui/pages/AccountsLabelSearch.pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';

import type { AddressesMetadataSearchResult } from 'types/api/addresses';

import * as addressMocks from 'mocks/address/address';
import { test, expect } from 'playwright/lib';

import AccountsLabelSearch from './AccountsLabelSearch';

const addresses: AddressesMetadataSearchResult = {
items: [
{
...addressMocks.withName,
tx_count: '1',
coin_balance: '12345678901234567890000',
},
{
...addressMocks.token,
tx_count: '109123890123',
coin_balance: '22222345678901234567890000',
ens_domain_name: null,
},
{
...addressMocks.withoutName,
tx_count: '11',
coin_balance: '1000000000000000000',
},
{
...addressMocks.eoa,
tx_count: '420',
coin_balance: null,
},
],
next_page_params: null,
};

const hooksConfig = {
router: {
query: {
slug: 'euler-finance-exploit',
tagType: 'generic',
tagName: 'Euler finance exploit',
},
},
};

test('base view +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
await mockTextAd();
await mockApiResponse(
'addresses_metadata_search',
addresses,
{
queryParams: {
slug: 'euler-finance-exploit',
tag_type: 'generic',
},
},
);
const component = await render(<AccountsLabelSearch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
Loading

0 comments on commit a3cdf62

Please sign in to comment.