pools pages (#2468)
* pools pages * fixes and changes * revert demo config
@@ -23,6 +23,7 @@ export { default as mixpanel } from './mixpanel';
|
||||
export { default as mudFramework } from './mudFramework';
|
||||
export { default as multichainButton } from './multichainButton';
|
||||
export { default as nameService } from './nameService';
|
||||
export { default as pools } from './pools';
|
||||
export { default as publicTagsSubmission } from './publicTagsSubmission';
|
||||
export { default as restApiDocs } from './restApiDocs';
|
||||
export { default as rewards } from './rewards';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
|
||||
const dexPoolsEnabled = getEnvValue('NEXT_PUBLIC_DEX_POOLS_ENABLED') === 'true';
|
||||
|
||||
const title = 'DEX Pools';
|
||||
|
||||
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
|
||||
if (contractInfoApiHost && dexPoolsEnabled) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
api: {
|
||||
endpoint: contractInfoApiHost,
|
||||
basePath: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -17,7 +17,7 @@ NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
|
||||
@@ -69,3 +69,4 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
|
||||
|
||||
@@ -870,6 +870,16 @@ const schema = yup
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_DEX_POOLS_ENABLED: yup.boolean()
|
||||
.when('NEXT_PUBLIC_CONTRACT_INFO_API_HOST', {
|
||||
is: (value: string) => Boolean(value),
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.test(
|
||||
'not-exist',
|
||||
'NEXT_PUBLIC_DEX_POOLS_ENABLED can only be used with NEXT_PUBLIC_CONTRACT_INFO_API_HOST',
|
||||
value => value === undefined,
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_ADDRESS_USERNAME_TAG: yup
|
||||
.mixed()
|
||||
|
||||
@@ -68,6 +68,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
|
||||
- [Get gas button](ENVS.md#get-gas-button)
|
||||
- [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk)
|
||||
- [Rewards service API](ENVS.md#rewards-service-api)
|
||||
- [DEX pools](ENVS.md#dex-pools)
|
||||
- [3rd party services configuration](ENVS.md#external-services-configuration)
|
||||
|
||||
|
||||
@@ -852,6 +853,15 @@ This feature enables Blockscout Merits program. It requires that the [My account
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_REWARDS_SERVICE_API_HOST | `string` | API URL | - | - | `https://example.com` | v1.36.0+ |
|
||||
|
||||
|
||||
|
||||
### DEX pools
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_DEX_POOLS_ENABLED | `boolean` | Set to true to enable the feature | Required | - | `true` | v1.37.0+ |
|
||||
| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.0.x+ |
|
||||
|
||||
## External services configuration
|
||||
|
||||
### Google ReCaptcha
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M13.776 20.692a8.5 8.5 0 0 1-1.695.288 7.3 7.3 0 1 0 7.632-10.68q.22.82.272 1.696a5.7 5.7 0 1 1-6.21 8.696m5.33-4.392q.312-.624.52-1.3H21v5h-5v-.287a8.5 8.5 0 0 0 1.315-1.013H19.7v-2.4z" clip-rule="evenodd"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M5.8 12.5a5.7 5.7 0 1 1 11.4 0 5.7 5.7 0 0 1-11.4 0m5.7-7.3a7.3 7.3 0 1 0 0 14.6 7.3 7.3 0 0 0 0-14.6m.563 4.675L11.5 8.9l-.563.975-2.165 3.75-.563.975h6.582l-.563-.975zM10.46 13.3l1.04-1.8 1.039 1.8z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 615 B |
@@ -94,6 +94,7 @@ import type {
|
||||
OptimismL2BatchTxs,
|
||||
OptimismL2BatchBlocks,
|
||||
} from 'types/api/optimisticL2';
|
||||
import type { Pool, PoolsResponse } from 'types/api/pools';
|
||||
import type { RawTracesResponse } from 'types/api/rawTrace';
|
||||
import type {
|
||||
RewardsConfigResponse,
|
||||
@@ -1128,6 +1129,22 @@ export const RESOURCES = {
|
||||
path: '/api/v2/advanced-filters/csv',
|
||||
},
|
||||
|
||||
// POOLS
|
||||
pools: {
|
||||
path: '/api/v1/chains/:chainId/pools',
|
||||
pathParams: [ 'chainId' as const ],
|
||||
filterFields: [ 'query' as const ],
|
||||
endpoint: getFeaturePayload(config.features.pools)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.pools)?.api.basePath,
|
||||
},
|
||||
|
||||
pool: {
|
||||
path: '/api/v1/chains/:chainId/pools/:hash',
|
||||
pathParams: [ 'chainId' as const, 'hash' as const ],
|
||||
endpoint: getFeaturePayload(config.features.pools)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.pools)?.api.basePath,
|
||||
},
|
||||
|
||||
// CONFIGS
|
||||
config_backend_version: {
|
||||
path: '/api/v2/config/backend-version',
|
||||
@@ -1222,7 +1239,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
|
||||
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
|
||||
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' |
|
||||
'token_transfers_all' | 'scroll_l2_txn_batches' | 'scroll_l2_txn_batch_txs' | 'scroll_l2_txn_batch_blocks' |
|
||||
'scroll_l2_deposits' | 'scroll_l2_withdrawals' | 'advanced_filter';
|
||||
'scroll_l2_deposits' | 'scroll_l2_withdrawals' | 'advanced_filter' | 'pools';
|
||||
|
||||
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
|
||||
|
||||
@@ -1416,6 +1433,8 @@ Q extends 'scroll_l2_withdrawals' ? ScrollL2MessagesResponse :
|
||||
Q extends 'scroll_l2_withdrawals_count' ? number :
|
||||
Q extends 'advanced_filter' ? AdvancedFilterResponse :
|
||||
Q extends 'advanced_filter_methods' ? AdvancedFilterMethodsResponse :
|
||||
Q extends 'pools' ? PoolsResponse :
|
||||
Q extends 'pool' ? Pool :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
@@ -1452,6 +1471,7 @@ Q extends 'address_mud_tables' ? AddressMudTablesFilter :
|
||||
Q extends 'address_mud_records' ? AddressMudRecordsFilter :
|
||||
Q extends 'token_transfers_all' ? TokenTransferFilters :
|
||||
Q extends 'advanced_filter' ? AdvancedFilterParams :
|
||||
Q extends 'pools' ? { query: string } :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
|
||||
export default function getItemIndex(index: number, page: number, pageSize: number = DEFAULT_PAGE_SIZE) {
|
||||
return (page - 1) * pageSize + index + 1;
|
||||
};
|
||||
@@ -197,7 +197,13 @@ export default function useNavItems(): ReturnType {
|
||||
icon: 'token-transfers',
|
||||
isActive: pathname === '/token-transfers',
|
||||
},
|
||||
];
|
||||
config.features.pools.isEnabled && {
|
||||
text: 'DEX tracker',
|
||||
nextRoute: { pathname: '/pools' as const },
|
||||
icon: 'dex-tracker',
|
||||
isActive: pathname === '/pools' || pathname.startsWith('/pool/'),
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
const apiNavItems: Array<NavItem> = [
|
||||
config.features.restApiDocs.isEnabled ? {
|
||||
|
||||
@@ -54,6 +54,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/mud-worlds': 'Root page',
|
||||
'/token-transfers': 'Root page',
|
||||
'/advanced-filter': 'Root page',
|
||||
'/pools': 'Root page',
|
||||
'/pools/[hash]': 'Regular page',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Regular page',
|
||||
|
||||
@@ -57,6 +57,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/mud-worlds': DEFAULT_TEMPLATE,
|
||||
'/token-transfers': DEFAULT_TEMPLATE,
|
||||
'/advanced-filter': DEFAULT_TEMPLATE,
|
||||
'/pools': DEFAULT_TEMPLATE,
|
||||
'/pools/[hash]': DEFAULT_TEMPLATE,
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -54,6 +54,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/mud-worlds': '%network_name% MUD worlds list',
|
||||
'/token-transfers': '%network_name% token transfers',
|
||||
'/advanced-filter': '%network_name% advanced filter',
|
||||
'/pools': '%network_name% DEX pools',
|
||||
'/pools/[hash]': '%network_name% pool details',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': '%network_name% login',
|
||||
|
||||
@@ -52,6 +52,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/mud-worlds': 'MUD worlds',
|
||||
'/token-transfers': 'Token transfers',
|
||||
'/advanced-filter': 'Advanced filter',
|
||||
'/pools': 'DEX pools',
|
||||
'/pools/[hash]': 'Pool details',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Login',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
type PoolLink = {
|
||||
url: string;
|
||||
image: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export default function getPoolLinks(pool?: Pool): Array<PoolLink> {
|
||||
if (!pool) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
url: pool.coin_gecko_terminal_url,
|
||||
image: '/static/gecko_terminal.png',
|
||||
title: 'GeckoTerminal',
|
||||
},
|
||||
].filter(link => Boolean(link.url));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
export const getPoolTitle = (pool: Pool) => {
|
||||
return `${ pool.base_token_symbol } / ${ pool.quote_token_symbol } ${ pool.fee ? `(${ pool.fee }%)` : '' }`;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
export const base: Pool = {
|
||||
contract_address: '0x06da0fd433c1a5d7a4faa01111c044910a184553',
|
||||
chain_id: '1',
|
||||
base_token_address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
base_token_symbol: 'USDT',
|
||||
base_token_icon_url: 'https://localhost:3000/utia.jpg',
|
||||
quote_token_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||
quote_token_symbol: 'WETH',
|
||||
quote_token_icon_url: 'https://localhost:3000/secondary_utia.jpg',
|
||||
fully_diluted_valuation_usd: '75486579078',
|
||||
market_cap_usd: '139312819076.195',
|
||||
liquidity: '2099941.2238',
|
||||
dex: { id: 'sushiswap', name: 'SushiSwap' },
|
||||
fee: '0.03',
|
||||
coin_gecko_terminal_url: 'https://www.geckoterminal.com/eth/pools/0x06da0fd433c1a5d7a4faa01111c044910a184553',
|
||||
};
|
||||
|
||||
export const noIcons: Pool = {
|
||||
...base,
|
||||
base_token_icon_url: null,
|
||||
quote_token_icon_url: null,
|
||||
};
|
||||
@@ -315,3 +315,13 @@ export const mud: GetServerSideProps<Props> = async(context) => {
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const pools: GetServerSideProps<Props> = async(context) => {
|
||||
if (!config.features.pools.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,8 @@ declare module "nextjs-routes" {
|
||||
| DynamicRoute<"/op/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/ops">
|
||||
| StaticRoute<"/output-roots">
|
||||
| DynamicRoute<"/pools/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/pools">
|
||||
| StaticRoute<"/public-tags/submit">
|
||||
| StaticRoute<"/search-results">
|
||||
| StaticRoute<"/sprite">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { Props } from 'nextjs/getServerSideProps';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const Pool = dynamic(() => import('ui/pages/Pool'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/pools/[hash]" query={ props.query }>
|
||||
<Pool/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { pools as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const Pools = dynamic(() => import('ui/pages/Pools'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/pools">
|
||||
<Pools/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { pools as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -46,6 +46,7 @@
|
||||
| "copy"
|
||||
| "cross"
|
||||
| "delete"
|
||||
| "dex-tracker"
|
||||
| "docs"
|
||||
| "donate"
|
||||
| "dots"
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,15 @@
|
||||
export const POOL = {
|
||||
contract_address: '0x6a1041865b76d1dc33da0257582591227c57832c',
|
||||
chain_id: '1',
|
||||
base_token_address: '0xf63e309818e4ea13782678ce6c31c1234fa61809',
|
||||
base_token_symbol: 'JANET',
|
||||
base_token_icon_url: null,
|
||||
quote_token_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||
quote_token_symbol: 'WETH',
|
||||
quote_token_icon_url: 'https://coin-images.coingecko.com/coins/images/2518/small/weth.png?1696503332',
|
||||
fully_diluted_valuation_usd: '15211385',
|
||||
market_cap_usd: '15211385',
|
||||
liquidity: '394101.2428',
|
||||
dex: { id: 'uniswap_v2', name: 'Uniswap V2' },
|
||||
coin_gecko_terminal_url: 'https://www.geckoterminal.com/eth/pools/0x6a1041865b76d1dc33da0257582591227c57832c',
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export type PoolsResponse = {
|
||||
items: Array<Pool>;
|
||||
next_page_params: {
|
||||
page_token: string;
|
||||
page_size: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type Pool = {
|
||||
contract_address: string;
|
||||
chain_id: string;
|
||||
base_token_address: string;
|
||||
base_token_symbol: string;
|
||||
base_token_icon_url: string | null;
|
||||
quote_token_address: string;
|
||||
quote_token_symbol: string;
|
||||
quote_token_icon_url: string | null;
|
||||
fully_diluted_valuation_usd: string;
|
||||
market_cap_usd: string;
|
||||
liquidity: string;
|
||||
dex: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
fee?: string;
|
||||
coin_gecko_terminal_url: string;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ test.describe('mobile', () => {
|
||||
|
||||
test('base view', async({ render, page }) => {
|
||||
await render(<MarketplaceAppInfo data={ appsMock[0] }/>);
|
||||
await page.getByLabel('Show project info').click();
|
||||
await page.getByLabel('Show info').click();
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,50 +1,20 @@
|
||||
import {
|
||||
PopoverTrigger, PopoverContent, PopoverBody,
|
||||
Modal, ModalContent, ModalCloseButton, useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
import InfoButton from 'ui/shared/InfoButton';
|
||||
|
||||
import Content from './MarketplaceAppInfo/Content';
|
||||
import TriggerButton from './MarketplaceAppInfo/TriggerButton';
|
||||
|
||||
interface Props {
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
}
|
||||
|
||||
const MarketplaceAppInfo = ({ data }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<TriggerButton onClick={ onToggle } onlyIcon/>
|
||||
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
|
||||
<ModalContent>
|
||||
<ModalCloseButton/>
|
||||
<Content data={ data }/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
<TriggerButton onClick={ onToggle } isActive={ isOpen }/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="500px">
|
||||
<PopoverBody px={ 6 } py={ 5 }>
|
||||
<Content data={ data }/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InfoButton>
|
||||
<Content data={ data }/>
|
||||
</InfoButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
onlyIcon?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const TriggerButton = ({ onClick, onlyIcon, isActive }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ ref }
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onClick }
|
||||
isActive={ isActive }
|
||||
aria-label="Show project info"
|
||||
fontWeight={ 500 }
|
||||
px={ onlyIcon ? 1 : 2 }
|
||||
h="32px"
|
||||
>
|
||||
<IconSvg name="info" boxSize={ 6 } mr={ onlyIcon ? 0 : 1 }/>
|
||||
{ !onlyIcon && <span>Info</span> }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(TriggerButton);
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -2,6 +2,7 @@ import { Hide, Show } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import getItemIndex from 'lib/getItemIndex';
|
||||
import { TOP_ADDRESS } from 'stubs/address';
|
||||
import { generateListStub } from 'stubs/utils';
|
||||
import AddressesListItem from 'ui/addresses/AddressesListItem';
|
||||
@@ -12,8 +13,6 @@ import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const Accounts = () => {
|
||||
const { isError, isPlaceholderData, data, pagination } = useQueryWithPages({
|
||||
resourceName: 'addresses',
|
||||
@@ -39,7 +38,7 @@ const Accounts = () => {
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
const pageStartIndex = (pagination.page - 1) * PAGE_SIZE + 1;
|
||||
const pageStartIndex = getItemIndex(0, pagination.page);
|
||||
const totalSupply = React.useMemo(() => {
|
||||
return BigNumber(data?.total_supply || '0');
|
||||
}, [ data?.total_supply ]);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import * as addressMock from 'mocks/address/address';
|
||||
import * as poolMock from 'mocks/pools/pool';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import Pool from './Pool';
|
||||
|
||||
const addressHash = '0x1234';
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash },
|
||||
},
|
||||
};
|
||||
|
||||
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse, mockTextAd, mockAssetResponse }) => {
|
||||
await mockTextAd();
|
||||
await mockApiResponse('pool', poolMock.base, { pathParams: { chainId: config.chain.id, hash: addressHash } });
|
||||
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: poolMock.base.contract_address } });
|
||||
await mockAssetResponse(poolMock.base.quote_token_icon_url as string, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(poolMock.base.base_token_icon_url as string, './playwright/mocks/image_md.jpg');
|
||||
const component = await render(<Pool/>, { hooksConfig });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Tag, Box, Flex, Image, Skeleton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/contexts/app';
|
||||
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
|
||||
import getPoolLinks from 'lib/pools/getPoolLinks';
|
||||
import { getPoolTitle } from 'lib/pools/getPoolTitle';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import * as addressStubs from 'stubs/address';
|
||||
import { POOL } from 'stubs/pools';
|
||||
import PoolInfo from 'ui/pool/PoolInfo';
|
||||
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import * as PoolEntity from 'ui/shared/entities/pool/PoolEntity';
|
||||
import InfoButton from 'ui/shared/InfoButton';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import VerifyWith from 'ui/shared/VerifyWith';
|
||||
|
||||
const Pool = () => {
|
||||
const router = useRouter();
|
||||
const appProps = useAppContext();
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
|
||||
const { data, isPlaceholderData, isError, error } = useApiQuery('pool', {
|
||||
pathParams: { hash, chainId: config.chain.id },
|
||||
queryOptions: {
|
||||
placeholderData: POOL,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const addressQuery = useApiQuery('address', {
|
||||
pathParams: { hash: data?.contract_address },
|
||||
queryOptions: {
|
||||
enabled: Boolean(data?.contract_address),
|
||||
placeholderData: addressStubs.ADDRESS_INFO,
|
||||
},
|
||||
});
|
||||
|
||||
const content = (() => {
|
||||
if (isError) {
|
||||
if (isCustomAppError(error)) {
|
||||
throwOnResourceLoadError({ resource: 'pool', error, isError: true });
|
||||
}
|
||||
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PoolInfo
|
||||
data={ data }
|
||||
isPlaceholderData={ isPlaceholderData }
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
const externalLinks = getPoolLinks(data);
|
||||
const hasLinks = externalLinks.length > 0;
|
||||
|
||||
const externalLinksComponents = React.useMemo(() => {
|
||||
return externalLinks
|
||||
.map((link) => {
|
||||
return (
|
||||
<LinkExternal h="34px" key={ link.url } href={ link.url } alignItems="center" display="inline-flex" minW="120px">
|
||||
<Image boxSize={ 5 } mr={ 2 } src={ link.image } alt={ `${ link.title } icon` }/>
|
||||
{ link.title }
|
||||
</LinkExternal>
|
||||
);
|
||||
});
|
||||
}, [ externalLinks ]);
|
||||
|
||||
const titleSecondRow = (
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
||||
{ addressQuery.data ? <AddressEntity address={ addressQuery.data } isLoading={ addressQuery.isPlaceholderData }/> : <Box/> }
|
||||
<Flex gap={ 2 }>
|
||||
<InfoButton>
|
||||
{ `This Liquidity Provider (LP) token represents ${ data?.base_token_symbol }/${ data?.quote_token_symbol } pairing.` }
|
||||
</InfoButton>
|
||||
{ hasLinks && (
|
||||
<VerifyWith
|
||||
links={ externalLinksComponents }
|
||||
label="Verify with"
|
||||
longText="View in"
|
||||
shortText=""
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const backLink = React.useMemo(() => {
|
||||
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/pools');
|
||||
|
||||
if (!hasGoBackLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'Back to pools list',
|
||||
url: appProps.referrer,
|
||||
};
|
||||
}, [ appProps.referrer ]);
|
||||
|
||||
const poolTitle = data ? getPoolTitle(data) : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={ poolTitle }
|
||||
backLink={ backLink }
|
||||
beforeTitle={ data ? (
|
||||
<PoolEntity.Icon
|
||||
pool={ data }
|
||||
isLoading={ isPlaceholderData }
|
||||
size="lg"
|
||||
/>
|
||||
) : null }
|
||||
contentAfter={ <Skeleton isLoaded={ !isPlaceholderData }><Tag>Pool</Tag></Skeleton> }
|
||||
secondRow={ titleSecondRow }
|
||||
isLoading={ isPlaceholderData }
|
||||
withTextAd
|
||||
/>
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pool;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import * as poolMock from 'mocks/pools/pool';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
import Pools from './Pools';
|
||||
|
||||
test('base view +@dark-mode', async({ render, mockApiResponse, mockTextAd, mockAssetResponse }) => {
|
||||
await mockTextAd();
|
||||
await mockApiResponse(
|
||||
'pools',
|
||||
{ items: [ poolMock.base, poolMock.noIcons, poolMock.base ], next_page_params: null },
|
||||
{ pathParams: { chainId: config.chain.id } },
|
||||
);
|
||||
await mockAssetResponse(poolMock.base.quote_token_icon_url as string, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(poolMock.base.base_token_icon_url as string, './playwright/mocks/image_s.jpg');
|
||||
const component = await render(<Pools/>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test.describe('mobile', () => {
|
||||
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
|
||||
|
||||
test('base view', async({ render, mockApiResponse, mockTextAd, mockAssetResponse }) => {
|
||||
await mockTextAd();
|
||||
await mockApiResponse(
|
||||
'pools',
|
||||
{ items: [ poolMock.base, poolMock.noIcons, poolMock.base ], next_page_params: null },
|
||||
{ pathParams: { chainId: config.chain.id } },
|
||||
);
|
||||
await mockAssetResponse(poolMock.base.quote_token_icon_url as string, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(poolMock.base.base_token_icon_url as string, './playwright/mocks/image_s.jpg');
|
||||
const component = await render(<Pools/>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Show, Hide, Flex } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { POOL } from 'stubs/pools';
|
||||
import PoolsListItem from 'ui/pools/PoolsListItem';
|
||||
import PoolsTable from 'ui/pools/PoolsTable';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
const Pools = () => {
|
||||
const router = useRouter();
|
||||
const q = getQueryParamString(router.query.query);
|
||||
|
||||
const [ searchTerm, setSearchTerm ] = React.useState<string>(q ?? '');
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300);
|
||||
|
||||
const poolsQuery = useQueryWithPages({
|
||||
resourceName: 'pools',
|
||||
pathParams: { chainId: config.chain.id },
|
||||
filters: { query: debouncedSearchTerm },
|
||||
options: {
|
||||
placeholderData: { items: Array(50).fill(POOL), next_page_params: { page_token: 'a', page_size: 50 } },
|
||||
},
|
||||
});
|
||||
|
||||
const handleSearchTermChange = React.useCallback((value: string) => {
|
||||
poolsQuery.onFilterChange({ query: value });
|
||||
setSearchTerm(value);
|
||||
}, [ poolsQuery ]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ poolsQuery.data?.items.map((item, index) => (
|
||||
<PoolsListItem
|
||||
key={ item.contract_address + (poolsQuery.isPlaceholderData ? index : '') }
|
||||
isLoading={ poolsQuery.isPlaceholderData }
|
||||
item={ item }
|
||||
/>
|
||||
)) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<PoolsTable
|
||||
items={ poolsQuery.data?.items ?? [] }
|
||||
top={ poolsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
|
||||
isLoading={ poolsQuery.isPlaceholderData }
|
||||
page={ poolsQuery.pagination.page }
|
||||
/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
|
||||
const filter = (
|
||||
<FilterInput
|
||||
w={{ base: '100%', lg: '360px' }}
|
||||
size="xs"
|
||||
onChange={ handleSearchTermChange }
|
||||
placeholder="Pair, token symbol or token address"
|
||||
initialValue={ searchTerm }
|
||||
/>
|
||||
);
|
||||
|
||||
const actionBar = (
|
||||
<>
|
||||
<Flex mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
|
||||
{ filter }
|
||||
</Flex>
|
||||
<ActionBar
|
||||
mt={ -6 }
|
||||
display={{ base: poolsQuery.pagination.isVisible ? 'flex' : 'none', lg: 'flex' }}
|
||||
>
|
||||
<Hide below="lg">
|
||||
{ filter }
|
||||
</Hide>
|
||||
<Pagination { ...poolsQuery.pagination } ml="auto"/>
|
||||
</ActionBar>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title="DEX tracker"
|
||||
withTextAd
|
||||
/>
|
||||
<DataListDisplay
|
||||
isError={ poolsQuery.isError }
|
||||
items={ poolsQuery.data?.items }
|
||||
emptyText="There are no pools."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
filterProps={{
|
||||
emptyFilteredText: `Couldn${ apos }t find pools that matches your filter query.`,
|
||||
hasActiveFilters: Boolean(debouncedSearchTerm),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pools;
|
||||
@@ -56,7 +56,7 @@ test('with verified info', async({ render, page, createSocket, mockApiResponse,
|
||||
const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
|
||||
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
|
||||
|
||||
await page.getByRole('button', { name: /project info/i }).click();
|
||||
await page.getByLabel('Show info').click();
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(pwConfig.adsBannerSelector) ],
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,114 @@
|
||||
import { Grid, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
|
||||
type Props = {
|
||||
data: Pool;
|
||||
isPlaceholderData: boolean;
|
||||
};
|
||||
|
||||
const PoolInfo = ({ data, isPlaceholderData }: Props) => {
|
||||
return (
|
||||
<Grid
|
||||
columnGap={ 8 }
|
||||
rowGap={{ base: 1, lg: 3 }}
|
||||
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
|
||||
overflow="hidden"
|
||||
>
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="The base token in a liquidity pool pair"
|
||||
>
|
||||
Base token
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<TokenEntity
|
||||
token={{
|
||||
type: 'ERC-20',
|
||||
address: data.base_token_address,
|
||||
name: data.base_token_symbol,
|
||||
symbol: data.base_token_symbol,
|
||||
icon_url: data.base_token_icon_url,
|
||||
}}
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="The quote token in a liquidity pool pair"
|
||||
>
|
||||
Quote token
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<TokenEntity
|
||||
token={{
|
||||
type: 'ERC-20',
|
||||
address: data.quote_token_address,
|
||||
name: data.quote_token_symbol,
|
||||
symbol: data.quote_token_symbol,
|
||||
icon_url: data.quote_token_icon_url,
|
||||
}}
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="Fully Diluted Valuation: theoretical market cap if all tokens were in circulation"
|
||||
>
|
||||
FDV
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
${ Number(data.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="Current market capitalization of the pool"
|
||||
>
|
||||
Market cap
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
${ Number(data.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="Current liquidity of the pool"
|
||||
>
|
||||
Liquidity
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
${ Number(data.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
isLoading={ isPlaceholderData }
|
||||
hint="DEX where the pool is traded"
|
||||
>
|
||||
DEX
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ data.dex.name }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsSponsoredItem isLoading={ isPlaceholderData }/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoolInfo;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Skeleton, Image } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
import getPoolLinks from 'lib/pools/getPoolLinks';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import PoolEntity from 'ui/shared/entities/pool/PoolEntity';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
|
||||
type Props = {
|
||||
item: Pool;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const UserOpsListItem = ({ item, isLoading }: Props) => {
|
||||
const externalLinks = getPoolLinks(item);
|
||||
return (
|
||||
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Pool</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<PoolEntity pool={ item } fontWeight={ 700 } isLoading={ isLoading }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Contract</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity address={{ hash: item.contract_address }} noIcon isLoading={ isLoading }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>FDV</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Market cap</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Liquidity</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>View in</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
{ externalLinks.map((link) => (
|
||||
<LinkExternal href={ link.url } key={ link.url } display="inline-flex">
|
||||
<Image src={ link.image } alt={ link.title } boxSize={ 5 } mr={ 2 }/>
|
||||
{ link.title }
|
||||
</LinkExternal>
|
||||
)) }
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserOpsListItem;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Table, Tbody, Th, Tr, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import PoolsTableItem from './PoolsTableItem';
|
||||
|
||||
type Props = {
|
||||
items: Array<Pool>;
|
||||
page: number;
|
||||
isLoading?: boolean;
|
||||
top?: number;
|
||||
};
|
||||
|
||||
const PoolsTable = ({ items, page, isLoading, top }: Props) => {
|
||||
return (
|
||||
<Table>
|
||||
<Thead top={ top ?? ACTION_BAR_HEIGHT_DESKTOP }>
|
||||
<Tr>
|
||||
<Th width="70%">Pool</Th>
|
||||
<Th width="30%">DEX </Th>
|
||||
<Th width="120px" isNumeric>
|
||||
<Flex alignItems="center" justifyContent="end">
|
||||
FDV
|
||||
<Hint
|
||||
label="Fully Diluted Valuation: theoretical market cap if all tokens were in circulation"
|
||||
boxSize={ 5 }
|
||||
ml={ 1 }
|
||||
/>
|
||||
</Flex>
|
||||
</Th>
|
||||
<Th width="120px" isNumeric>Market cap</Th>
|
||||
<Th width="120px" isNumeric>Liquidity</Th>
|
||||
<Th width="75px" isNumeric>View in</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ items.map((item, index) => (
|
||||
<PoolsTableItem key={ item.contract_address + (isLoading ? index : '') } item={ item } index={ index } page={ page } isLoading={ isLoading }/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoolsTable;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Flex, Box, Td, Tr, Skeleton, Text, Image, Tooltip } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
import getItemIndex from 'lib/getItemIndex';
|
||||
import getPoolLinks from 'lib/pools/getPoolLinks';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import PoolEntity from 'ui/shared/entities/pool/PoolEntity';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
|
||||
type Props = {
|
||||
item: Pool;
|
||||
index: number;
|
||||
page: number;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const PoolsTableItem = ({
|
||||
item,
|
||||
page,
|
||||
index,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const externalLinks = getPoolLinks(item);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>
|
||||
<Flex gap={ 2 } alignItems="start">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Text px={ 2 }>{ getItemIndex(index, page) }</Text>
|
||||
</Skeleton>
|
||||
<Box>
|
||||
<PoolEntity pool={ item } fontWeight={ 700 } mb={ 2 } isLoading={ isLoading }/>
|
||||
<AddressEntity address={{ hash: item.contract_address }} noIcon isLoading={ isLoading }/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>
|
||||
<Skeleton isLoaded={ !isLoading }>{ item.dex.name }</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
${ Number(item.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Skeleton isLoaded={ !isLoading } display="flex" gap={ 2 } justifyContent="center">
|
||||
{ externalLinks.map((link) => (
|
||||
<Tooltip label={ link.title } key={ link.url }>
|
||||
<Box display="inline-block">
|
||||
<LinkExternal href={ link.url } display="inline-flex">
|
||||
<Image src={ link.image } alt={ link.title } boxSize={ 5 }/>
|
||||
</LinkExternal>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)) }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoolsTableItem;
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
PopoverTrigger, PopoverContent, PopoverBody,
|
||||
Modal, ModalContent, ModalCloseButton,
|
||||
useDisclosure,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
|
||||
import IconSvg from './IconSvg';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const InfoButton = ({ children }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const triggerButton = (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onToggle }
|
||||
isActive={ isOpen }
|
||||
aria-label="Show info"
|
||||
fontWeight={ 500 }
|
||||
lineHeight={ 6 }
|
||||
pl={ 1 }
|
||||
pr={ isMobile ? 1 : 2 }
|
||||
h="32px"
|
||||
>
|
||||
<IconSvg name="info" boxSize={ 6 } mr={ isMobile ? 0 : 1 }/>
|
||||
{ !isMobile && <span>Info</span> }
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
{ triggerButton }
|
||||
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
|
||||
<ModalContent>
|
||||
<ModalCloseButton/>
|
||||
{ children }
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
{ triggerButton }
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="500px">
|
||||
<PopoverBody px={ 6 } py={ 5 }>
|
||||
{ children }
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InfoButton);
|
||||
@@ -1,15 +1,7 @@
|
||||
import {
|
||||
Image,
|
||||
Button,
|
||||
PopoverTrigger,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
Show,
|
||||
Hide,
|
||||
useColorModeValue,
|
||||
chakra,
|
||||
useDisclosure,
|
||||
Grid,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
@@ -17,10 +9,9 @@ import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
|
||||
|
||||
import config from 'configs/app';
|
||||
import stripTrailingSlash from 'lib/stripTrailingSlash';
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
|
||||
import VerifyWith from 'ui/shared/VerifyWith';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -29,7 +20,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const defaultIconColor = useColorModeValue('gray.400', 'gray.500');
|
||||
|
||||
const explorersLinks = React.useMemo(() => {
|
||||
@@ -54,46 +44,13 @@ const NetworkExplorers = ({ className, type, pathParam }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
<PopoverTriggerTooltip label="Verify with other explorers" className={ className }>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onToggle }
|
||||
isActive={ isOpen }
|
||||
aria-label="Verify in other explorers"
|
||||
fontWeight={ 500 }
|
||||
px={ 2 }
|
||||
h="32px"
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<IconSvg name="explorer" boxSize={ 5 }/>
|
||||
<Show above="xl">
|
||||
<chakra.span ml={ 1 }>{ explorersLinks.length } Explorer{ explorersLinks.length > 1 ? 's' : '' }</chakra.span>
|
||||
</Show>
|
||||
<Hide above="xl">
|
||||
<chakra.span ml={ 1 }>{ explorersLinks.length }</chakra.span>
|
||||
</Hide>
|
||||
</Button>
|
||||
</PopoverTriggerTooltip>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="auto">
|
||||
<PopoverBody >
|
||||
<chakra.span color="text_secondary" fontSize="xs">Verify with other explorers</chakra.span>
|
||||
<Grid
|
||||
alignItems="center"
|
||||
templateColumns={ explorersLinks.length > 1 ? 'auto auto' : '1fr' }
|
||||
columnGap={ 4 }
|
||||
rowGap={ 2 }
|
||||
mt={ 3 }
|
||||
>
|
||||
{ explorersLinks }
|
||||
</Grid>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<VerifyWith
|
||||
className={ className }
|
||||
links={ explorersLinks }
|
||||
label="Verify with other explorers"
|
||||
longText={ `${ explorersLinks.length } Explorer${ explorersLinks.length > 1 ? 's' : '' }` }
|
||||
shortText={ explorersLinks.length.toString() }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Button,
|
||||
PopoverTrigger,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
Show,
|
||||
Hide,
|
||||
chakra,
|
||||
useDisclosure,
|
||||
Grid,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
links: Array<React.ReactNode>;
|
||||
label: string;
|
||||
longText: string;
|
||||
shortText?: string;
|
||||
}
|
||||
|
||||
const VerifyWith = ({ className, links, label, longText, shortText }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
<PopoverTriggerTooltip label={ label } className={ className }>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onToggle }
|
||||
isActive={ isOpen }
|
||||
aria-label={ label }
|
||||
fontWeight={ 500 }
|
||||
px={ shortText ? 2 : 1 }
|
||||
h="32px"
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<IconSvg name="explorer" boxSize={ 5 }/>
|
||||
<Show above="xl">
|
||||
<chakra.span ml={ 1 }>{ longText }</chakra.span>
|
||||
</Show>
|
||||
{ shortText && (
|
||||
<Hide above="xl">
|
||||
<chakra.span ml={ 1 }>{ shortText }</chakra.span>
|
||||
</Hide>
|
||||
) }
|
||||
</Button>
|
||||
</PopoverTriggerTooltip>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="auto">
|
||||
<PopoverBody >
|
||||
<chakra.span color="text_secondary" fontSize="xs">{ label }</chakra.span>
|
||||
<Grid
|
||||
alignItems="center"
|
||||
templateColumns={ links.length > 1 ? 'auto auto' : '1fr' }
|
||||
columnGap={ 4 }
|
||||
rowGap={ 2 }
|
||||
mt={ 3 }
|
||||
>
|
||||
{ links }
|
||||
</Grid>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(VerifyWith);
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as poolMock from 'mocks/pools/pool';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import PoolEntity from './PoolEntity';
|
||||
|
||||
test.use({ viewport: { width: 300, height: 100 } });
|
||||
|
||||
test('with icons +@dark-mode', async({ render, mockAssetResponse }) => {
|
||||
await mockAssetResponse('https://localhost:3000/utia.jpg', './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse('https://localhost:3000/secondary_utia.jpg', './playwright/mocks/image_md.jpg');
|
||||
const component = await render(
|
||||
<PoolEntity
|
||||
pool={ poolMock.base }
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('no icons +@dark-mode', async({ render }) => {
|
||||
const component = await render(
|
||||
<PoolEntity
|
||||
pool={ poolMock.noIcons }
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { As } from '@chakra-ui/react';
|
||||
import { Flex, Skeleton, chakra, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Pool } from 'types/api/pools';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import { getPoolTitle } from 'lib/pools/getPoolTitle';
|
||||
import * as EntityBase from 'ui/shared/entities/base/components';
|
||||
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
|
||||
|
||||
import { distributeEntityProps } from '../base/utils';
|
||||
import * as TokenEntity from '../token/TokenEntity';
|
||||
|
||||
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'pool'>;
|
||||
|
||||
const Link = chakra((props: LinkProps) => {
|
||||
const defaultHref = route({ pathname: '/pools/[hash]', query: { hash: props.pool.contract_address } });
|
||||
|
||||
return (
|
||||
<EntityBase.Link
|
||||
{ ...props }
|
||||
href={ props.href ?? defaultHref }
|
||||
>
|
||||
{ props.children }
|
||||
</EntityBase.Link>
|
||||
);
|
||||
});
|
||||
|
||||
type IconProps = Pick<EntityProps, 'pool' | 'className'> & EntityBase.IconBaseProps;
|
||||
|
||||
const Icon = (props: IconProps) => {
|
||||
const bgColor = useColorModeValue('white', 'black');
|
||||
const borderColor = useColorModeValue('whiteAlpha.800', 'blackAlpha.800');
|
||||
return (
|
||||
<Flex>
|
||||
<Flex
|
||||
bgColor={ bgColor }
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor={ borderColor }
|
||||
>
|
||||
<TokenEntity.Icon
|
||||
marginRight={ 0 }
|
||||
size={ props.size }
|
||||
token={{
|
||||
icon_url: props.pool.base_token_icon_url,
|
||||
symbol: props.pool.base_token_symbol,
|
||||
address: props.pool.base_token_address,
|
||||
name: '',
|
||||
type: 'ERC-20',
|
||||
}}
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
transform="translateX(-8px)"
|
||||
bgColor={ bgColor }
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor={ borderColor }
|
||||
>
|
||||
<TokenEntity.Icon
|
||||
marginRight={ 0 }
|
||||
size={ props.size }
|
||||
token={{
|
||||
icon_url: props.pool.quote_token_icon_url,
|
||||
symbol: props.pool.quote_token_symbol,
|
||||
address: props.pool.quote_token_address,
|
||||
name: '',
|
||||
type: 'ERC-20',
|
||||
}}
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'pool'>;
|
||||
|
||||
const Content = chakra((props: ContentProps) => {
|
||||
const nameString = getPoolTitle(props.pool);
|
||||
|
||||
return (
|
||||
<TruncatedTextTooltip label={ nameString }>
|
||||
<Skeleton
|
||||
isLoaded={ !props.isLoading }
|
||||
display="inline-block"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
height="fit-content"
|
||||
>
|
||||
{ nameString }
|
||||
</Skeleton>
|
||||
</TruncatedTextTooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const Container = EntityBase.Container;
|
||||
|
||||
export interface EntityProps extends EntityBase.EntityBaseProps {
|
||||
pool: Pool;
|
||||
}
|
||||
|
||||
const PoolEntity = (props: EntityProps) => {
|
||||
const partsProps = distributeEntityProps(props);
|
||||
|
||||
return (
|
||||
<Container w="100%" { ...partsProps.container }>
|
||||
<Icon { ...partsProps.icon }/>
|
||||
<Link { ...partsProps.link }>
|
||||
<Content { ...partsProps.content }/>
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra<As, EntityProps>(PoolEntity));
|
||||
|
||||
export {
|
||||
Container,
|
||||
Link,
|
||||
Icon,
|
||||
Content,
|
||||
};
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -37,7 +37,7 @@ const Icon = (props: IconProps) => {
|
||||
const styles = {
|
||||
marginRight: props.marginRight ?? 2,
|
||||
boxSize: props.boxSize ?? getIconProps(props.size).boxSize,
|
||||
borderRadius: 'base',
|
||||
borderRadius: props.token.type === 'ERC-20' ? 'full' : 'base',
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,6 @@ const Icon = (props: IconProps) => {
|
||||
return (
|
||||
<Image
|
||||
{ ...styles }
|
||||
borderRadius={ props.token.type === 'ERC-20' ? 'full' : 'base' }
|
||||
className={ props.className }
|
||||
src={ props.token.icon_url ?? undefined }
|
||||
alt={ `${ props.token.name || 'token' } logo` }
|
||||
|
||||
@@ -1,55 +1,24 @@
|
||||
import {
|
||||
PopoverTrigger, PopoverContent, PopoverBody,
|
||||
Modal, ModalContent, ModalCloseButton,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenVerifiedInfo } from 'types/api/token';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
import InfoButton from 'ui/shared/InfoButton';
|
||||
|
||||
import Content, { hasContent } from './TokenProjectInfo/Content';
|
||||
import TriggerButton from './TokenProjectInfo/TriggerButton';
|
||||
|
||||
interface Props {
|
||||
data: TokenVerifiedInfo;
|
||||
}
|
||||
|
||||
const TokenProjectInfo = ({ data }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
if (!hasContent(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<TriggerButton onClick={ onToggle } isActive={ isOpen }/>
|
||||
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
|
||||
<ModalContent>
|
||||
<ModalCloseButton/>
|
||||
<Content data={ data }/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
<TriggerButton onClick={ onToggle } isActive={ isOpen }/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="500px">
|
||||
<PopoverBody px={ 6 } py={ 5 }>
|
||||
<Content data={ data }/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InfoButton>
|
||||
<Content data={ data }/>
|
||||
</InfoButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const TriggerButton = ({ onClick, isActive }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ ref }
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onClick }
|
||||
isActive={ isActive }
|
||||
aria-label="Show project info"
|
||||
fontWeight={ 500 }
|
||||
lineHeight={ 6 }
|
||||
pl={ 1 }
|
||||
pr={ 2 }
|
||||
h="32px"
|
||||
>
|
||||
<IconSvg name="info" boxSize={ 6 } mr={ 1 }/>
|
||||
<span>Info</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(TriggerButton);
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getItemIndex from 'lib/getItemIndex';
|
||||
import { getTokenTypeName } from 'lib/token/tokenTypes';
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
@@ -19,8 +20,6 @@ type Props = {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const bridgedTokensFeature = config.features.bridgedTokens;
|
||||
|
||||
const TokensTableItem = ({
|
||||
@@ -65,7 +64,7 @@ const TokensTableItem = ({
|
||||
{ bridgedChainTag && <Tag isLoading={ isLoading }>{ bridgedChainTag }</Tag> }
|
||||
</Flex>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" ml="auto" color="text_secondary" minW="24px" textAlign="right" lineHeight={ 6 }>
|
||||
<span>{ (page - 1) * PAGE_SIZE + index + 1 }</span>
|
||||
<span>{ getItemIndex(index, page) }</span>
|
||||
</Skeleton>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getItemIndex from 'lib/getItemIndex';
|
||||
import { getTokenTypeName } from 'lib/token/tokenTypes';
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
@@ -19,8 +20,6 @@ type Props = {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const bridgedTokensFeature = config.features.bridgedTokens;
|
||||
|
||||
const TokensTableItem = ({
|
||||
@@ -70,7 +69,7 @@ const TokensTableItem = ({
|
||||
mr={ 3 }
|
||||
minW="28px"
|
||||
>
|
||||
{ (page - 1) * PAGE_SIZE + index + 1 }
|
||||
{ getItemIndex(index, page) }
|
||||
</Skeleton>
|
||||
<Flex overflow="hidden" flexDir="column" rowGap={ 2 }>
|
||||
<TokenEntity
|
||||
|
||||