diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 34e82b8248..c5815c2330 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -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'; diff --git a/configs/app/features/pools.ts b/configs/app/features/pools.ts new file mode 100644 index 0000000000..310100da4f --- /dev/null +++ b/configs/app/features/pools.ts @@ -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; diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index cf5ae1a135..6de7b51a9a 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -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 diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index d20fc6d176..072daff4a7 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -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() diff --git a/docs/ENVS.md b/docs/ENVS.md index 5d082f2420..b2842c2e73 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -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 diff --git a/icons/dex-tracker.svg b/icons/dex-tracker.svg new file mode 100644 index 0000000000..64deb3aed4 --- /dev/null +++ b/icons/dex-tracker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/api/resources.ts b/lib/api/resources.ts index baeaf0e4f8..b99d04846f 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 = ResourcePayload; @@ -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 */ diff --git a/lib/getItemIndex.ts b/lib/getItemIndex.ts new file mode 100644 index 0000000000..6103c23ddd --- /dev/null +++ b/lib/getItemIndex.ts @@ -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; +}; diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index af93805ce1..bb7c48300f 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -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 = [ config.features.restApiDocs.isEnabled ? { diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index f4c6c9705f..4535257365 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -54,6 +54,8 @@ const OG_TYPE_DICT: Record = { '/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', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 00abae5c8e..a8069e0f28 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -57,6 +57,8 @@ const TEMPLATE_MAP: Record = { '/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, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 5c21d49c9d..46eedf1d30 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -54,6 +54,8 @@ const TEMPLATE_MAP: Record = { '/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', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 6018920c44..e361248bbf 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -52,6 +52,8 @@ export const PAGE_TYPE_DICT: Record = { '/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', diff --git a/lib/pools/getPoolLinks.ts b/lib/pools/getPoolLinks.ts new file mode 100644 index 0000000000..19d738f01e --- /dev/null +++ b/lib/pools/getPoolLinks.ts @@ -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 { + if (!pool) { + return []; + } + + return [ + { + url: pool.coin_gecko_terminal_url, + image: '/static/gecko_terminal.png', + title: 'GeckoTerminal', + }, + ].filter(link => Boolean(link.url)); +} diff --git a/lib/pools/getPoolTitle.ts b/lib/pools/getPoolTitle.ts new file mode 100644 index 0000000000..1e4db61ac6 --- /dev/null +++ b/lib/pools/getPoolTitle.ts @@ -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 }%)` : '' }`; +}; diff --git a/mocks/pools/pool.ts b/mocks/pools/pool.ts new file mode 100644 index 0000000000..78dce73b1d --- /dev/null +++ b/mocks/pools/pool.ts @@ -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, +}; diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 9522b73323..3927590800 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -315,3 +315,13 @@ export const mud: GetServerSideProps = async(context) => { return base(context); }; + +export const pools: GetServerSideProps = async(context) => { + if (!config.features.pools.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index cb870694cb..185a221d3c 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -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"> diff --git a/pages/pools/[hash].tsx b/pages/pools/[hash].tsx new file mode 100644 index 0000000000..4bb18b039f --- /dev/null +++ b/pages/pools/[hash].tsx @@ -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) => { + return ( + + + + ); +}; + +export default Page; + +export { pools as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/pools/index.tsx b/pages/pools/index.tsx new file mode 100644 index 0000000000..10530b11bd --- /dev/null +++ b/pages/pools/index.tsx @@ -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 ( + + + + ); +}; + +export default Page; + +export { pools as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 1e5264ca10..4126633c3e 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -46,6 +46,7 @@ | "copy" | "cross" | "delete" + | "dex-tracker" | "docs" | "donate" | "dots" diff --git a/public/static/gecko_terminal.png b/public/static/gecko_terminal.png new file mode 100644 index 0000000000..b6f3e13b3b Binary files /dev/null and b/public/static/gecko_terminal.png differ diff --git a/stubs/pools.ts b/stubs/pools.ts new file mode 100644 index 0000000000..47aadaf6a0 --- /dev/null +++ b/stubs/pools.ts @@ -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', +}; diff --git a/types/api/pools.tsx b/types/api/pools.tsx new file mode 100644 index 0000000000..21560f6735 --- /dev/null +++ b/types/api/pools.tsx @@ -0,0 +1,27 @@ +export type PoolsResponse = { + items: Array; + 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; +}; diff --git a/ui/marketplace/MarketplaceAppInfo.pw.tsx b/ui/marketplace/MarketplaceAppInfo.pw.tsx index 17e686ab33..1d15158a03 100644 --- a/ui/marketplace/MarketplaceAppInfo.pw.tsx +++ b/ui/marketplace/MarketplaceAppInfo.pw.tsx @@ -16,7 +16,7 @@ test.describe('mobile', () => { test('base view', async({ render, page }) => { await render(); - await page.getByLabel('Show project info').click(); + await page.getByLabel('Show info').click(); await expect(page).toHaveScreenshot(); }); }); diff --git a/ui/marketplace/MarketplaceAppInfo.tsx b/ui/marketplace/MarketplaceAppInfo.tsx index 9641056c3a..107186017c 100644 --- a/ui/marketplace/MarketplaceAppInfo.tsx +++ b/ui/marketplace/MarketplaceAppInfo.tsx @@ -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 ( - <> - - - - - - - - - ); - } - return ( - - - - - - - - - - + + + ); }; diff --git a/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx b/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx deleted file mode 100644 index 172cb4f32a..0000000000 --- a/ui/marketplace/MarketplaceAppInfo/TriggerButton.tsx +++ /dev/null @@ -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) => { - return ( - - ); -}; - -export default React.forwardRef(TriggerButton); diff --git a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 80fe876b12..6c912d627e 100644 Binary files a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png index d3a539555e..d6e441a77e 100644 Binary files a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/Accounts.tsx b/ui/pages/Accounts.tsx index 987e1aa8d2..602cb676b0 100644 --- a/ui/pages/Accounts.tsx +++ b/ui/pages/Accounts.tsx @@ -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 = () => { ); - 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 ]); diff --git a/ui/pages/Pool.pw.tsx b/ui/pages/Pool.pw.tsx new file mode 100644 index 0000000000..ea5d553232 --- /dev/null +++ b/ui/pages/Pool.pw.tsx @@ -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(, { hooksConfig }); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/Pool.tsx b/ui/pages/Pool.tsx new file mode 100644 index 0000000000..d88877857d --- /dev/null +++ b/ui/pages/Pool.tsx @@ -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 ; + } + + if (!data) { + return null; + } + + return ( + + ); + })(); + + const externalLinks = getPoolLinks(data); + const hasLinks = externalLinks.length > 0; + + const externalLinksComponents = React.useMemo(() => { + return externalLinks + .map((link) => { + return ( + + { + { link.title } + + ); + }); + }, [ externalLinks ]); + + const titleSecondRow = ( + + { addressQuery.data ? : } + + + { `This Liquidity Provider (LP) token represents ${ data?.base_token_symbol }/${ data?.quote_token_symbol } pairing.` } + + { hasLinks && ( + + ) } + + + ); + + 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 ( + <> + + ) : null } + contentAfter={ Pool } + secondRow={ titleSecondRow } + isLoading={ isPlaceholderData } + withTextAd + /> + { content } + + ); +}; + +export default Pool; diff --git a/ui/pages/Pools.pw.tsx b/ui/pages/Pools.pw.tsx new file mode 100644 index 0000000000..3af9cc8080 --- /dev/null +++ b/ui/pages/Pools.pw.tsx @@ -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(); + 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(); + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/Pools.tsx b/ui/pages/Pools.tsx new file mode 100644 index 0000000000..7879092ef6 --- /dev/null +++ b/ui/pages/Pools.tsx @@ -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(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 = ( + <> + + { poolsQuery.data?.items.map((item, index) => ( + + )) } + + + + + + ); + + const filter = ( + + ); + + const actionBar = ( + <> + + { filter } + + + + { filter } + + + + + ); + + return ( + <> + + + + ); +}; + +export default Pools; diff --git a/ui/pages/Token.pw.tsx b/ui/pages/Token.pw.tsx index b2987b38b7..4f1aa483ab 100644 --- a/ui/pages/Token.pw.tsx +++ b/ui/pages/Token.pw.tsx @@ -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) ], diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index bb1ea3e2d6..c86f9687ba 100644 Binary files a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png index 45d5a9e54b..f6f872df39 100644 Binary files a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pool.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Pool.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..3f05bb9e09 Binary files /dev/null and b/ui/pages/__screenshots__/Pool.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pool.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Pool.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..9a1d6e1c5a Binary files /dev/null and b/ui/pages/__screenshots__/Pool.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pool.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Pool.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..6014df238e Binary files /dev/null and b/ui/pages/__screenshots__/Pool.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pools.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Pools.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..d9bf9df153 Binary files /dev/null and b/ui/pages/__screenshots__/Pools.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pools.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Pools.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..cd86cf3bcb Binary files /dev/null and b/ui/pages/__screenshots__/Pools.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Pools.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Pools.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..84ed179a77 Binary files /dev/null and b/ui/pages/__screenshots__/Pools.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png index 7292349d1c..ef59ef7cc9 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png index 00cade0cab..0796d85fd2 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png differ diff --git a/ui/pool/PoolInfo.tsx b/ui/pool/PoolInfo.tsx new file mode 100644 index 0000000000..fed9a75faf --- /dev/null +++ b/ui/pool/PoolInfo.tsx @@ -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 ( + + + Base token + + + + + + + Quote token + + + + + + + FDV + + + + ${ Number(data.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + Market cap + + + + ${ Number(data.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + Liquidity + + + + ${ Number(data.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + DEX + + + + { data.dex.name } + + + + + + ); +}; + +export default PoolInfo; diff --git a/ui/pools/PoolsListItem.tsx b/ui/pools/PoolsListItem.tsx new file mode 100644 index 0000000000..4c0f92cdca --- /dev/null +++ b/ui/pools/PoolsListItem.tsx @@ -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 ( + + + Pool + + + + + Contract + + + + + FDV + + + ${ Number(item.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + Market cap + + + ${ Number(item.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + Liquidity + + + ${ Number(item.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + View in + + + { externalLinks.map((link) => ( + + { + { link.title } + + )) } + + + + ); +}; + +export default UserOpsListItem; diff --git a/ui/pools/PoolsTable.tsx b/ui/pools/PoolsTable.tsx new file mode 100644 index 0000000000..167e9ec697 --- /dev/null +++ b/ui/pools/PoolsTable.tsx @@ -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; + page: number; + isLoading?: boolean; + top?: number; +}; + +const PoolsTable = ({ items, page, isLoading, top }: Props) => { + return ( + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
PoolDEX + + FDV + + + Market capLiquidityView in
+ ); +}; + +export default PoolsTable; diff --git a/ui/pools/PoolsTableItem.tsx b/ui/pools/PoolsTableItem.tsx new file mode 100644 index 0000000000..3f29542136 --- /dev/null +++ b/ui/pools/PoolsTableItem.tsx @@ -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 ( + + + + + { getItemIndex(index, page) } + + + + + + + + + { item.dex.name } + + + + ${ Number(item.fully_diluted_valuation_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + ${ Number(item.market_cap_usd).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + ${ Number(item.liquidity).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }) } + + + + + { externalLinks.map((link) => ( + + + + { + + + + )) } + + + + ); +}; + +export default PoolsTableItem; diff --git a/ui/shared/InfoButton.tsx b/ui/shared/InfoButton.tsx new file mode 100644 index 0000000000..fde2aea670 --- /dev/null +++ b/ui/shared/InfoButton.tsx @@ -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 = ( + + ); + + if (isMobile) { + return ( + <> + { triggerButton } + + + + { children } + + + + ); + } + + return ( + + + { triggerButton } + + + + { children } + + + + ); +}; + +export default React.memo(InfoButton); diff --git a/ui/shared/NetworkExplorers.tsx b/ui/shared/NetworkExplorers.tsx index 15475956c2..3cf00da3e9 100644 --- a/ui/shared/NetworkExplorers.tsx +++ b/ui/shared/NetworkExplorers.tsx @@ -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 ( - - - - - - - - - Verify with other explorers - 1 ? 'auto auto' : '1fr' } - columnGap={ 4 } - rowGap={ 2 } - mt={ 3 } - > - { explorersLinks } - - - - + 1 ? 's' : '' }` } + shortText={ explorersLinks.length.toString() } + /> ); }; diff --git a/ui/shared/VerifyWith.tsx b/ui/shared/VerifyWith.tsx new file mode 100644 index 0000000000..2be1e416a6 --- /dev/null +++ b/ui/shared/VerifyWith.tsx @@ -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; + label: string; + longText: string; + shortText?: string; +} + +const VerifyWith = ({ className, links, label, longText, shortText }: Props) => { + const { isOpen, onToggle, onClose } = useDisclosure(); + + return ( + + + + + + + + + { label } + 1 ? 'auto auto' : '1fr' } + columnGap={ 4 } + rowGap={ 2 } + mt={ 3 } + > + { links } + + + + + ); +}; + +export default chakra(VerifyWith); diff --git a/ui/shared/entities/pool/PoolEntity.pw.tsx b/ui/shared/entities/pool/PoolEntity.pw.tsx new file mode 100644 index 0000000000..4a4d5fad2c --- /dev/null +++ b/ui/shared/entities/pool/PoolEntity.pw.tsx @@ -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( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('no icons +@dark-mode', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/entities/pool/PoolEntity.tsx b/ui/shared/entities/pool/PoolEntity.tsx new file mode 100644 index 0000000000..eddbc71401 --- /dev/null +++ b/ui/shared/entities/pool/PoolEntity.tsx @@ -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; + +const Link = chakra((props: LinkProps) => { + const defaultHref = route({ pathname: '/pools/[hash]', query: { hash: props.pool.contract_address } }); + + return ( + + { props.children } + + ); +}); + +type IconProps = Pick & EntityBase.IconBaseProps; + +const Icon = (props: IconProps) => { + const bgColor = useColorModeValue('white', 'black'); + const borderColor = useColorModeValue('whiteAlpha.800', 'blackAlpha.800'); + return ( + + + + + + + + + ); +}; + +type ContentProps = Omit & Pick; + +const Content = chakra((props: ContentProps) => { + const nameString = getPoolTitle(props.pool); + + return ( + + + { nameString } + + + ); +}); + +const Container = EntityBase.Container; + +export interface EntityProps extends EntityBase.EntityBaseProps { + pool: Pool; +} + +const PoolEntity = (props: EntityProps) => { + const partsProps = distributeEntityProps(props); + + return ( + + + + + + + ); +}; + +export default React.memo(chakra(PoolEntity)); + +export { + Container, + Link, + Icon, + Content, +}; diff --git a/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_no-icons-dark-mode-1.png b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_no-icons-dark-mode-1.png new file mode 100644 index 0000000000..5dae996eff Binary files /dev/null and b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_no-icons-dark-mode-1.png differ diff --git a/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_with-icons-dark-mode-1.png b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_with-icons-dark-mode-1.png new file mode 100644 index 0000000000..e7390bbe25 Binary files /dev/null and b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_dark-color-mode_with-icons-dark-mode-1.png differ diff --git a/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_no-icons-dark-mode-1.png b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_no-icons-dark-mode-1.png new file mode 100644 index 0000000000..50c2f1639d Binary files /dev/null and b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_no-icons-dark-mode-1.png differ diff --git a/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_with-icons-dark-mode-1.png b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_with-icons-dark-mode-1.png new file mode 100644 index 0000000000..836ed25bfd Binary files /dev/null and b/ui/shared/entities/pool/__screenshots__/PoolEntity.pw.tsx_default_with-icons-dark-mode-1.png differ diff --git a/ui/shared/entities/token/TokenEntity.tsx b/ui/shared/entities/token/TokenEntity.tsx index 491f43569e..63fce45d65 100644 --- a/ui/shared/entities/token/TokenEntity.tsx +++ b/ui/shared/entities/token/TokenEntity.tsx @@ -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 ( { { - const isMobile = useIsMobile(); - const { isOpen, onToggle, onClose } = useDisclosure(); - if (!hasContent(data)) { return null; } - if (isMobile) { - return ( - <> - - - - - - - - - ); - } - return ( - - - - - - - - - - + + + ); }; diff --git a/ui/token/TokenProjectInfo/TriggerButton.tsx b/ui/token/TokenProjectInfo/TriggerButton.tsx deleted file mode 100644 index fa5a03f317..0000000000 --- a/ui/token/TokenProjectInfo/TriggerButton.tsx +++ /dev/null @@ -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) => { - return ( - - ); -}; - -export default React.forwardRef(TriggerButton); diff --git a/ui/tokens/TokensListItem.tsx b/ui/tokens/TokensListItem.tsx index b40a1f0d1c..45f223c3f9 100644 --- a/ui/tokens/TokensListItem.tsx +++ b/ui/tokens/TokensListItem.tsx @@ -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 && { bridgedChainTag } } - { (page - 1) * PAGE_SIZE + index + 1 } + { getItemIndex(index, page) } diff --git a/ui/tokens/TokensTableItem.tsx b/ui/tokens/TokensTableItem.tsx index 87f5882216..9fd6578517 100644 --- a/ui/tokens/TokensTableItem.tsx +++ b/ui/tokens/TokensTableItem.tsx @@ -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) }