diff --git a/src/apps/pillarx-app/components/MediaGridCollection/tests/__snapshots__/DisplayCollectionImage.test.tsx.snap b/src/apps/pillarx-app/components/MediaGridCollection/tests/__snapshots__/DisplayCollectionImage.test.tsx.snap index 723a3282..aa2c84be 100644 --- a/src/apps/pillarx-app/components/MediaGridCollection/tests/__snapshots__/DisplayCollectionImage.test.tsx.snap +++ b/src/apps/pillarx-app/components/MediaGridCollection/tests/__snapshots__/DisplayCollectionImage.test.tsx.snap @@ -44,439 +44,50 @@ exports[` renders correctly and matches snapshot witho - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + `; diff --git a/src/apps/pillarx-app/components/PointsTile/test/__snapshots__/PointsTile.test.tsx.snap b/src/apps/pillarx-app/components/PointsTile/test/__snapshots__/PointsTile.test.tsx.snap index e8695ba1..36e51197 100644 --- a/src/apps/pillarx-app/components/PointsTile/test/__snapshots__/PointsTile.test.tsx.snap +++ b/src/apps/pillarx-app/components/PointsTile/test/__snapshots__/PointsTile.test.tsx.snap @@ -160,7 +160,7 @@ exports[` renders correctly and matches snapshot 1`] = ` class="desktop:text-[22px] tablet:text-lg mobile:text-lg text-white" data-testid="points-formatted-timestamp" > - -41 + -29 @@ -413,7 +413,7 @@ exports[` renders correctly and matches snapshot 1`] = ` class="desktop:text-[22px] tablet:text-lg mobile:text-lg text-white" data-testid="points-formatted-timestamp" > - -41 + -29 diff --git a/src/apps/pillarx-app/components/TileTitle/TileTitle.tsx b/src/apps/pillarx-app/components/TileTitle/TileTitle.tsx new file mode 100644 index 00000000..1fbea51f --- /dev/null +++ b/src/apps/pillarx-app/components/TileTitle/TileTitle.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react'; + +// components +import Body from '../Typography/Body'; + +type TileTitleProps = { + title?: string; + leftDecorator?: string; + rightDecorator?: string; +}; +const TileTitle = ({ + title, + leftDecorator, + rightDecorator, +}: TileTitleProps) => { + const [isBrokenImageRight, setIsBrokenImageRight] = useState(false); + const [isBrokenImageLeft, setIsBrokenImageLeft] = useState(false); + + return ( +
+ {leftDecorator && !isBrokenImageLeft && ( + left-decorator-image setIsBrokenImageLeft(true)} + /> + )} + {title && {title}} + {rightDecorator && !isBrokenImageRight && ( + right-decorator-image setIsBrokenImageRight(true)} + /> + )} +
+ ); +}; + +export default TileTitle; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/LeftColumnTokenMarketDataRow.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/LeftColumnTokenMarketDataRow.tsx new file mode 100644 index 00000000..f22ad5cc --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/LeftColumnTokenMarketDataRow.tsx @@ -0,0 +1,90 @@ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ +import { formatDistanceToNowStrict, isValid, parseISO } from 'date-fns'; +import { DateTime } from 'luxon'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +// types +import { TokensMarketDataRow } from '../../../../types/api'; + +// utils +import { getShorterTimeUnits } from '../../../../utils/common'; + +// components +import Body from '../Typography/Body'; +import BodySmall from '../Typography/BodySmall'; + +// images +import CopyIcon from '../../images/token-market-data-copy.png'; + +type LeftColumnTokenMarketDataRowProps = { + data: TokensMarketDataRow; +}; + +const LeftColumnTokenMarketDataRow = ({ + data, +}: LeftColumnTokenMarketDataRowProps) => { + const { leftColumn } = data; + + const timestampToISO = + DateTime.fromSeconds(leftColumn?.line2?.timestamp || 0).toISO() || ''; + + const ISOToDate = parseISO(timestampToISO); + + let timestamp = isValid(ISOToDate) + ? formatDistanceToNowStrict( + DateTime.fromSeconds(leftColumn?.line2?.timestamp || 0).toISO() || '', + { addSuffix: true } + ) + : undefined; + + // Replace long units with shorter units and delete white space before the units + timestamp = timestamp && getShorterTimeUnits(timestamp); + + return ( +
+
+ {leftColumn?.line1?.text1 && ( + + {leftColumn?.line1?.text1} + + )} + {leftColumn?.line1?.text2 && ( + + {leftColumn?.line1?.text2} + + )} + {leftColumn?.line1?.copyLink && ( + + copy-token-address e.stopPropagation()} + /> + + )} +
+
+ {timestamp && ( + + {timestamp} + + )} + {leftColumn?.line2?.volume && ( + + Vol:{' '} + {leftColumn?.line2?.volume} + + )} + {leftColumn?.line2?.liquidity && ( + + Liq:{' '} + {leftColumn?.line2?.liquidity} + + )} +
+
+ ); +}; + +export default LeftColumnTokenMarketDataRow; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/RightColumnTokenMarketDataRow.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/RightColumnTokenMarketDataRow.tsx new file mode 100644 index 00000000..f4c49afa --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/RightColumnTokenMarketDataRow.tsx @@ -0,0 +1,58 @@ +import { TbTriangleFilled } from 'react-icons/tb'; + +// types +import { TokensMarketDataRow } from '../../../../types/api'; + +// components +import Body from '../Typography/Body'; +import BodySmall from '../Typography/BodySmall'; + +type RightColumnTokenMarketDataRowProps = { + data: TokensMarketDataRow; +}; + +const RightColumnTokenMarketDataRow = ({ + data, +}: RightColumnTokenMarketDataRowProps) => { + const { rightColumn } = data; + + return ( +
+
+ {rightColumn?.line1?.price && ( + + {rightColumn?.line1?.price} + + )} +
+ {(rightColumn?.line1?.direction === 'UP' || + rightColumn?.line1?.direction === 'DOWN') && ( + + )} + {rightColumn?.line1?.percentage && ( + + {rightColumn?.line1?.percentage} + + )} +
+
+ {rightColumn?.line2?.transactionCount && ( + + Txs:{' '} + {rightColumn?.line2?.transactionCount} + + )} +
+ ); +}; + +export default RightColumnTokenMarketDataRow; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/TokenLogoMarketDataRow.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/TokenLogoMarketDataRow.tsx new file mode 100644 index 00000000..ced6bb50 --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/TokenLogoMarketDataRow.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; + +// components +import RandomAvatar from '../RandomAvatar/RandomAvatar'; + +type TokenLogoMarketDataRowProps = { + tokenLogo?: string; + chainLogo?: string; + tokenName?: string; +}; + +const TokenLogoMarketDataRow = ({ + tokenLogo, + chainLogo, + tokenName, +}: TokenLogoMarketDataRowProps) => { + const [isBrokenImage, setIsBrokenImage] = useState(false); + const [isBrokenImageChain, setIsBrokenImageChain] = useState(false); + + return ( +
+ {tokenLogo && !isBrokenImage ? ( + token-logo setIsBrokenImage(true)} + /> + ) : ( +
+ +
+ )} + + {/* Overlay text when no logo available */} + {(!tokenLogo || isBrokenImage) && ( + + {tokenName?.slice(0, 2)} + + )} + + {/* Blockchain logo overlapping when only one blockchain for this token */} + {chainLogo && !isBrokenImageChain ? ( +
+ logo setIsBrokenImageChain(true)} + /> +
+ ) : null} +
+ ); +}; + +export default TokenLogoMarketDataRow; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/TokenMarketDataRow.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/TokenMarketDataRow.tsx new file mode 100644 index 00000000..e6705380 --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/TokenMarketDataRow.tsx @@ -0,0 +1,51 @@ +import { useNavigate } from 'react-router-dom'; + +// types +import { TokensMarketDataRow } from '../../../../types/api'; + +// components +import Body from '../Typography/Body'; +import LeftColumnTokenMarketDataRow from './LeftColumnTokenMarketDataRow'; +import RightColumnTokenMarketDataRow from './RightColumnTokenMarketDataRow'; +import TokenLogoMarketDataRow from './TokenLogoMarketDataRow'; + +type TokenMarketDataRowProps = { + data: TokensMarketDataRow; + listNumber: number; + isLastNumber: boolean; + isMiddleNumber: boolean; +}; +const TokenMarketDataRow = ({ + data, + listNumber, + isLastNumber, + isMiddleNumber, +}: TokenMarketDataRowProps) => { + const navigate = useNavigate(); + return ( +
(data.link ? navigate(`${data.link}`) : undefined)} + > +
+ + {listNumber > 0 && listNumber < 10 ? `0${listNumber}` : listNumber} + + + +
+
+ +
+
+ ); +}; + +export default TokenMarketDataRow; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/tests/LeftColumnTokenMarketDataRow.test.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/LeftColumnTokenMarketDataRow.test.tsx new file mode 100644 index 00000000..aa842d61 --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/LeftColumnTokenMarketDataRow.test.tsx @@ -0,0 +1,71 @@ +import { render, screen } from '@testing-library/react'; + +// components +import LeftColumnTokenMarketDataRow from '../LeftColumnTokenMarketDataRow'; + +const ethTokenRow = { + link: 'token-atlas?someLink=true', + leftColumn: { + token: { + primaryImage: 'eth.png', + }, + line1: { + text1: 'ETH', + text2: 'Ethereum', + copyLink: '0xD76b5c2A23ef78368d8E34288B5b65D616B746aE', + }, + line2: { + timestamp: 1745331519, + volume: '1.2m', + liquidity: '$30,123', + }, + }, + rightColumn: { + line1: { + price: '$0.042188', + direction: 'UP', + percentage: '20.1%', + }, + line2: { + transactionCount: '1823', + }, + }, +}; + +describe(' - ETH token row', () => { + it('renders and matches snapshot', () => { + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + + it('renders text1 and text2', () => { + render(); + expect(screen.getByText('ETH')).toBeInTheDocument(); + expect(screen.getByText('Ethereum')).toBeInTheDocument(); + }); + + it('renders volume and liquidity', () => { + render(); + expect(screen.getByText(/Vol:/)).toBeInTheDocument(); + expect(screen.getByText('1.2m')).toBeInTheDocument(); + expect(screen.getByText(/Liq:/)).toBeInTheDocument(); + expect(screen.getByText('$30,123')).toBeInTheDocument(); + }); + + it('does not break if some values are missing', () => { + const incomplete = { + leftColumn: { + line1: { + text1: 'ETH', + }, + line2: {}, + }, + }; + + render(); + + expect(screen.getByText('ETH')).toBeInTheDocument(); + expect(screen.queryByText(/Vol:/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Liq:/)).not.toBeInTheDocument(); + }); +}); diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/tests/RightColumnTokenMarketDataRow.test.tsx b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/RightColumnTokenMarketDataRow.test.tsx new file mode 100644 index 00000000..7d0f6ea0 --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/RightColumnTokenMarketDataRow.test.tsx @@ -0,0 +1,80 @@ +import { render, screen } from '@testing-library/react'; + +// components +import RightColumnTokenMarketDataRow from '../RightColumnTokenMarketDataRow'; + +const ethTokenRow = { + link: 'token-atlas?someLink=true', + leftColumn: { + token: { + primaryImage: 'eth.png', + }, + line1: { + text1: 'ETH', + text2: 'Ethereum', + copyLink: '0xD76b5c2A23ef78368d8E34288B5b65D616B746aE', + }, + line2: { + timestamp: 1745331519, + volume: '1.2m', + liquidity: '$30,123', + }, + }, + rightColumn: { + line1: { + price: '$0.042188', + direction: 'UP', + percentage: '20.1%', + }, + line2: { + transactionCount: '1823', + }, + }, +}; + +describe(' - ETH token row', () => { + it('renders and matches snapshot', () => { + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + + it('renders percentage, price and transaction count ', () => { + render(); + + expect(screen.getByText('$0.042188')).toBeInTheDocument(); + expect(screen.getByText('20.1%')).toBeInTheDocument(); + expect(screen.getByText(/Txs:/)).toBeInTheDocument(); + expect(screen.getByText('1823')).toBeInTheDocument(); + }); + + it('renders with missing percentage', () => { + const noPercentageRow = { + ...ethTokenRow, + rightColumn: { + line1: { + price: '$0.999', + direction: 'UP', + }, + }, + }; + + render(); + expect(screen.getByText('$0.999')).toBeInTheDocument(); + expect(screen.queryByText('%')).not.toBeInTheDocument(); + }); + + it('renders only transaction count when line1 is missing', () => { + const noLine1Row = { + ...ethTokenRow, + rightColumn: { + line2: { + transactionCount: '9999', + }, + }, + }; + + render(); + expect(screen.getByText('9999')).toBeInTheDocument(); + expect(screen.queryByText('$')).not.toBeInTheDocument(); + }); +}); diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/LeftColumnTokenMarketDataRow.test.tsx.snap b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/LeftColumnTokenMarketDataRow.test.tsx.snap new file mode 100644 index 00000000..e43edb1d --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/LeftColumnTokenMarketDataRow.test.tsx.snap @@ -0,0 +1,172 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` - ETH token row renders and matches snapshot 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+

+ ETH +

+

+ Ethereum +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.2m +

+

+ + Liq: + + + $30,123 +

+
+
+
+ , + "container":
+
+
+

+ ETH +

+

+ Ethereum +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.2m +

+

+ + Liq: + + + $30,123 +

+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/RightColumnTokenMarketDataRow.test.tsx.snap b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/RightColumnTokenMarketDataRow.test.tsx.snap new file mode 100644 index 00000000..20e21812 --- /dev/null +++ b/src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/RightColumnTokenMarketDataRow.test.tsx.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` - ETH token row renders and matches snapshot 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+

+ $0.042188 +

+
+ + + + +

+ 20.1% +

+
+
+

+ + Txs: + + + 1823 +

+
+
+ , + "container":
+
+
+

+ $0.042188 +

+
+ + + + +

+ 20.1% +

+
+
+

+ + Txs: + + + 1823 +

+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/apps/pillarx-app/components/TokensWithMarketDataTile/TokensWithMarketDataTile.tsx b/src/apps/pillarx-app/components/TokensWithMarketDataTile/TokensWithMarketDataTile.tsx new file mode 100644 index 00000000..a8f66fd0 --- /dev/null +++ b/src/apps/pillarx-app/components/TokensWithMarketDataTile/TokensWithMarketDataTile.tsx @@ -0,0 +1,90 @@ +// types +import { Projection, TokensMarketData } from '../../../../types/api'; + +// components +import TileContainer from '../TileContainer/TileContainer'; +import TileTitle from '../TileTitle/TileTitle'; +import TokenMarketDataRow from '../TokenMarketDataRow/TokenMarketDataRow'; + +type TokensWithMarketDataTileProps = { + data: Projection | undefined; + isDataLoading: boolean; +}; + +const TokensWithMarketDataTile = ({ + data, + isDataLoading, +}: TokensWithMarketDataTileProps) => { + const { data: tokensWithMarketData } = data || {}; + + const dataTokens = tokensWithMarketData as TokensMarketData | undefined; + + if (!data || !dataTokens?.rows?.length || isDataLoading) { + return null; + } + + const dataLength = dataTokens.rows.length; + const midLength = Math.ceil(dataLength / 2); + + return ( + // TO DO - replace the background with container background color once this has changed on Design + + +
+ {/* Mobile: 1 column */} +
+ {dataTokens.rows.map((row, index) => ( + + ))} +
+ + {/* Desktop: 2 columns */} +
+ {Array.from({ length: midLength }).map((_, i) => ( +
+ {dataTokens?.rows?.slice(0, midLength)[i] && ( + + )} + {dataTokens?.rows?.slice(midLength, dataTokens.rows.length)[ + i + ] && ( + + )} +
+ ))} +
+
+
+ ); +}; + +export default TokensWithMarketDataTile; diff --git a/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/TokensWithMarketDataTile.test.tsx b/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/TokensWithMarketDataTile.test.tsx new file mode 100644 index 00000000..63ce5c18 --- /dev/null +++ b/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/TokensWithMarketDataTile.test.tsx @@ -0,0 +1,207 @@ +import { render, screen, within } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +// components +import TokensWithMarketDataTile from '../TokensWithMarketDataTile'; + +// types +import { + ApiLayout, + Projection, + TokensMarketData, +} from '../../../../../types/api'; + +const mockTokensMarketData: Projection = { + id: 'tokens-with-market-data', + layout: ApiLayout.TOKENS_WITH_MARKET_DATA, + meta: {}, + data: { + title: { + text: 'Top Tokens', + leftDecorator: '🔥', + rightDecorator: '📈', + }, + rows: [ + { + link: 'token-atlas?someLink=true', + leftColumn: { + token: { + primaryImage: 'eth.png', + }, + line1: { + text1: 'ETH', + text2: 'Ethereum', + copyLink: '0xD76b5c2A23ef78368d8E34288B5b65D616B746aE', + }, + line2: { + timestamp: 1745331519, + volume: '1.2m', + liquidity: '$30,123', + }, + }, + rightColumn: { + line1: { + price: '$0.042188', + direction: 'UP', + percentage: '20.1%', + }, + line2: { + transactionCount: '1823', + }, + }, + }, + { + link: 'token-atlas?someLink=true', + leftColumn: { + token: { + primaryImage: + 'https://cryptologos.cc/logos/ethereum-eth-logo.svg?v=040', + secondaryImage: + 'https://cryptologos.cc/logos/optimism-ethereum-op-logo.svg?v=040', + }, + line1: { + text1: 'XDAI', + text2: 'XDAI', + copyLink: '0xD76b5c2A23ef78368d8E34288B5b65D616B746aE', + }, + line2: { + timestamp: 1745334519, + volume: '1.4m', + liquidity: '$3,123', + }, + }, + rightColumn: { + line1: { + price: '$1.062188', + direction: 'DOWN', + percentage: '3.1%', + }, + line2: { + transactionCount: '1423', + }, + }, + }, + ], + }, +}; + +describe('', () => { + it('renders and matches snapshot', () => { + const tree = render( + + + + ); + expect(tree).toMatchSnapshot(); + }); + + it('renders title and decorators', () => { + render( + + + + ); + + expect(screen.getByText('Top Tokens')).toBeInTheDocument(); + expect(screen.getByAltText('left-decorator-image')).toBeInTheDocument(); + expect(screen.getByAltText('right-decorator-image')).toBeInTheDocument(); + }); + + it('renders token rows with expected data (mobile layout)', () => { + const { container } = render( + + + + ); + + // Target the mobile layout otherwise it would not consider the hidden elements when in mobile or desktop + const mobileContainer = container.querySelector( + '.flex-col.desktop\\:hidden' + ) as HTMLElement; + expect(mobileContainer).toBeInTheDocument(); + + const mobileScreen = within(mobileContainer!); + + expect(mobileScreen.getByText('Ethereum')).toBeInTheDocument(); + expect(mobileScreen.getByText('ETH')).toBeInTheDocument(); + expect(mobileScreen.getByText('1.2m')).toBeInTheDocument(); + expect(mobileScreen.getByText('$30,123')).toBeInTheDocument(); + expect(mobileScreen.getByText('$0.042188')).toBeInTheDocument(); + expect(mobileScreen.getByText('20.1%')).toBeInTheDocument(); + expect(mobileScreen.getByText('1823')).toBeInTheDocument(); + + expect(mobileScreen.getAllByText('XDAI')).toHaveLength(2); + expect(mobileScreen.getByText('1.4m')).toBeInTheDocument(); + expect(mobileScreen.getByText('$3,123')).toBeInTheDocument(); + expect(mobileScreen.getByText('$1.062188')).toBeInTheDocument(); + expect(mobileScreen.getByText('3.1%')).toBeInTheDocument(); + expect(mobileScreen.getByText('1423')).toBeInTheDocument(); + }); + + it('does not render anything while loading', () => { + render( + + + + ); + + expect(screen.queryByText('Top Tokens')).not.toBeInTheDocument(); + expect(screen.queryByText('Ethereum')).not.toBeInTheDocument(); + }); + + it('does not render if data is missing or rows are empty', () => { + const emptyData: Projection = { + ...mockTokensMarketData, + data: { + ...mockTokensMarketData.data, + rows: [], + }, + }; + + render( + + + + ); + + expect(screen.queryByText('Top Tokens')).not.toBeInTheDocument(); + }); + + it('renders the right number of rowsd', () => { + const overfilledData: Projection = { + ...mockTokensMarketData, + data: { + ...mockTokensMarketData.data, + rows: new Array(10).fill( + (mockTokensMarketData.data as TokensMarketData).rows?.[0] + ), + }, + }; + + const { container } = render( + + + + ); + + // Target the mobile layout otherwise it would not consider the hidden elements when in mobile or desktop + const mobileContainer = container.querySelector( + '.flex-col.desktop\\:hidden' + ) as HTMLElement; + expect(mobileContainer).toBeInTheDocument(); + + const mobileScreen = within(mobileContainer!); + + const ethItems = mobileScreen.getAllByText('Ethereum'); + expect(ethItems.length).toBeLessThanOrEqual(10); + }); +}); diff --git a/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/__snapshots__/TokensWithMarketDataTile.test.tsx.snap b/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/__snapshots__/TokensWithMarketDataTile.test.tsx.snap new file mode 100644 index 00000000..14eeb11b --- /dev/null +++ b/src/apps/pillarx-app/components/TokensWithMarketDataTile/test/__snapshots__/TokensWithMarketDataTile.test.tsx.snap @@ -0,0 +1,1308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders and matches snapshot 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+ left-decorator-image +

+ Top Tokens +

+ right-decorator-image +
+
+
+
+
+

+ 01 +

+
+ token-logo +
+
+
+

+ ETH +

+

+ Ethereum +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.2m +

+

+ + Liq: + + + $30,123 +

+
+
+
+
+
+
+

+ $0.042188 +

+
+ + + + +

+ 20.1% +

+
+
+

+ + Txs: + + + 1823 +

+
+
+
+
+
+

+ 02 +

+
+ token-logo +
+ logo +
+
+
+
+

+ XDAI +

+

+ XDAI +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.4m +

+

+ + Liq: + + + $3,123 +

+
+
+
+
+
+
+

+ $1.062188 +

+
+ + + + +

+ 3.1% +

+
+
+

+ + Txs: + + + 1423 +

+
+
+
+
+ +
+
+
+ , + "container":
+
+
+ left-decorator-image +

+ Top Tokens +

+ right-decorator-image +
+
+
+
+
+

+ 01 +

+
+ token-logo +
+
+
+

+ ETH +

+

+ Ethereum +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.2m +

+

+ + Liq: + + + $30,123 +

+
+
+
+
+
+
+

+ $0.042188 +

+
+ + + + +

+ 20.1% +

+
+
+

+ + Txs: + + + 1823 +

+
+
+
+
+
+

+ 02 +

+
+ token-logo +
+ logo +
+
+
+
+

+ XDAI +

+

+ XDAI +

+ copy-token-address +
+
+

+ 2d ago +

+

+ + Vol: + + + 1.4m +

+

+ + Liq: + + + $3,123 +

+
+
+
+
+
+
+

+ $1.062188 +

+
+ + + + +

+ 3.1% +

+
+
+

+ + Txs: + + + 1423 +

+
+
+
+
+ +
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/apps/pillarx-app/images/token-market-data-copy.png b/src/apps/pillarx-app/images/token-market-data-copy.png new file mode 100644 index 00000000..3d7acfa3 Binary files /dev/null and b/src/apps/pillarx-app/images/token-market-data-copy.png differ diff --git a/src/apps/pillarx-app/tailwind.pillarx.config.js b/src/apps/pillarx-app/tailwind.pillarx.config.js index 53e39a3a..219521bd 100644 --- a/src/apps/pillarx-app/tailwind.pillarx.config.js +++ b/src/apps/pillarx-app/tailwind.pillarx.config.js @@ -22,6 +22,7 @@ module.exports = { purple_medium: '#8A77FF', percentage_green: '#05FFDD', percentage_red: '#FF366C', + market_row_green: '#5CFF93', }, fontFamily: { custom: ['Formular'], diff --git a/src/apps/pillarx-app/utils/configComponent.ts b/src/apps/pillarx-app/utils/configComponent.ts index 71c5b8d5..2170ab30 100644 --- a/src/apps/pillarx-app/utils/configComponent.ts +++ b/src/apps/pillarx-app/utils/configComponent.ts @@ -9,6 +9,7 @@ import HighlightedMediaGridTile from '../components/HighlightedMediaGridTile/Hig import PointsTile from '../components/PointsTile/PointsTile'; import TokensHorizontalTile from '../components/TokensHorizontalTile/TokensHorizontalTile'; import TokensVerticalTile from '../components/TokensVerticalTile/TokensVerticalTile'; +import TokensWithMarketDataTile from '../components/TokensWithMarketDataTile/TokensWithMarketDataTile'; type TileComponentType = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -22,4 +23,5 @@ export const componentMap: TileComponentType = { [ApiLayout.AD]: AdvertTile, [ApiLayout.MEDIA_GRID_HIGHLIGHTED]: HighlightedMediaGridTile, [ApiLayout.PXPOINTS]: PointsTile, + [ApiLayout.TOKENS_WITH_MARKET_DATA]: TokensWithMarketDataTile, }; diff --git a/src/types/api.ts b/src/types/api.ts index b2dff85f..889e62df 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -6,6 +6,7 @@ export enum ApiLayout { AD = 'AD', MEDIA_GRID_HIGHLIGHTED = 'MEDIA_GRID_HIGHLIGHTED', PXPOINTS = 'PXPOINTS', + TOKENS_WITH_MARKET_DATA = 'TOKENS_WITH_MARKET_DATA', } export enum LeaderboardRankChange { @@ -130,11 +131,55 @@ export type Points = { }; }; +export type TokensMarketDataRow = { + link?: string; + leftColumn?: { + token?: { + primaryImage?: string; + secondaryImage?: string; + }; + line1?: { + text1?: string; + text2?: string; + copyLink?: string; + }; + line2?: { + timestamp?: number; + volume?: string; + liquidity?: string; + }; + }; + rightColumn?: { + line1?: { + price?: string; + direction?: string; + percentage?: string; + }; + line2?: { + transactionCount?: string; + }; + }; +}; + +export type TokensMarketData = { + title?: { + text?: string; + leftDecorator?: string; + rightDecorator?: string; + }; + rows?: TokensMarketDataRow[]; +}; + export type Projection = { meta: { display?: GenericBannerDisplay | EditorialDisplay | TileTitle; }; - data?: TokenData[] | Advertisement | MediaGridData | Points; + data?: + | TokenData[] + | Advertisement + | MediaGridData + | Points + | TokensMarketData; layout: ApiLayout; id: string; }; diff --git a/src/utils/common.ts b/src/utils/common.ts index def7bf73..6e194eb6 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -39,3 +39,19 @@ export const copyToClipboard = async (text: string, onSuccess?: () => void) => { export const convertDateToUnixTimestamp = (date: Date): number => Math.floor(date.getTime() / 1000); + +export const getShorterTimeUnits = (formattedDistanceToNowDate: string) => { + // Replace long units with shorter units and delete white space before the units + return formattedDistanceToNowDate + .replace('seconds', 's') + .replace('second', 's') + .replace('minutes', 'min') + .replace('minute', 'min') + .replace('hours', 'h') + .replace('hour', 'h') + .replace('days', 'd') + .replace('day', 'd') + .replace('months', 'mo') + .replace('month', 'mo') + .replace(/(\d+)\s+(?=[a-zA-Z])/g, '$1'); +};