= 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 ( + + + ); + }); + }, [ externalLinks ]); + + const titleSecondRow = ( ++ { link.title } + + { addressQuery.data ? + ); + + 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 ( + <> +: } + + ++ { `This Liquidity Provider (LP) token represents ${ data?.base_token_symbol }/${ data?.quote_token_symbol } pairing.` } + + { hasLinks && ( ++ ) } + + ) : null } + contentAfter={ } + 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( Pool ); + 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 } + ++ + > + ); + + return ( + <> ++ { filter } + ++ + + > + ); +}; + +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 ( + + + ); +}; + +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 ( ++ 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 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: ArrayPool ++ + ++ 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 } + ; + page: number; + isLoading?: boolean; + top?: number; +}; + +const PoolsTable = ({ items, page, isLoading, top }: Props) => { + return ( + + +
+ ); +}; + +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 ( ++ + + + { items.map((item, index) => ( +Pool +DEX ++ ++ FDV + ++ Market cap +Liquidity +View in ++ )) } + + + + ); +}; + +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 } ++ ++ ++ +{ 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) => ( + ++ + )) } ++ ++ ++ + + > + ); + } + + return ( ++ ++ { 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 ( -+ { triggerButton } + ++ ++ { children } + +- +- -- - -- -- -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 ( + + + ); +}; + +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( ++ ++ + ++ ++ +{ label } +1 ? 'auto auto' : '1fr' } + columnGap={ 4 } + rowGap={ 2 } + mt={ 3 } + > + { links } + +, + ); + + 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 ( + + + ); +}); + +const Container = EntityBase.Container; + +export interface EntityProps extends EntityBase.EntityBaseProps { + pool: Pool; +} + +const PoolEntity = (props: EntityProps) => { + const partsProps = distributeEntityProps(props); + + return ( ++ { nameString } + ++ + ); +}; + +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) }