Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ exports[`<PointsTile /> 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
<span
class="text-base"
>
Expand Down Expand Up @@ -413,7 +413,7 @@ exports[`<PointsTile /> 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
<span
class="text-base"
>
Expand Down
42 changes: 42 additions & 0 deletions src/apps/pillarx-app/components/TileTitle/TileTitle.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [isBrokenImageLeft, setIsBrokenImageLeft] = useState<boolean>(false);

return (
<div className="flex items-center gap-2">
{leftDecorator && !isBrokenImageLeft && (
<img
src={leftDecorator}
alt="left-decorator-image"
className="w-7 h-7 object-contain"
onError={() => setIsBrokenImageLeft(true)}
/>
)}
{title && <Body className="font-normal">{title}</Body>}
{rightDecorator && !isBrokenImageRight && (
<img
src={rightDecorator}
alt="right-decorator-image"
className="w-6 h-6 object-fill rounded-full"
onError={() => setIsBrokenImageRight(true)}
/>
)}
</div>
);
};

export default TileTitle;
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col ml-1.5 h-full justify-between">
<div className="flex gap-1 items-center">
{leftColumn?.line1?.text1 && (
<Body className="font-normal text-white desktop:text-base tablet:text-base mobile:text-sm">
{leftColumn?.line1?.text1}
</Body>
)}
{leftColumn?.line1?.text2 && (
<Body className="font-normal text-white/[.5] desktop:text-base tablet:text-base mobile:text-sm">
{leftColumn?.line1?.text2}
</Body>
)}
{leftColumn?.line1?.copyLink && (
<CopyToClipboard text={leftColumn.line1.copyLink}>
<img
src={CopyIcon}
alt="copy-token-address"
className="w-2.5 h-3"
onClick={(e) => e.stopPropagation()}
/>
</CopyToClipboard>
)}
</div>
<div className="flex flex-wrap gap-2 mobile:gap-1.5">
{timestamp && (
<BodySmall className="mobile:hidden text-white desktop:text-sm tablet:text-sm mobile:text-xs">
{timestamp}
</BodySmall>
)}
{leftColumn?.line2?.volume && (
<BodySmall className="text-white desktop:text-sm tablet:text-sm mobile:text-xs">
<span className="text-white/[.5]">Vol:</span>{' '}
{leftColumn?.line2?.volume}
</BodySmall>
)}
{leftColumn?.line2?.liquidity && (
<BodySmall className="text-white desktop:text-sm tablet:text-sm mobile:text-xs">
<span className="text-white/[.5]">Liq:</span>{' '}
{leftColumn?.line2?.liquidity}
</BodySmall>
)}
</div>
</div>
);
};

export default LeftColumnTokenMarketDataRow;
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col h-full items-end justify-between">
<div className="flex desktop:gap-1 tablet:gap-1 mobile:flex-col mobile:items-end">
{rightColumn?.line1?.price && (
<Body
className={`font-normal ${rightColumn?.line1?.direction === 'UP' && 'desktop:text-market_row_green tablet:text-market_row_green'} ${rightColumn?.line1?.direction === 'DOWN' && 'desktop:text-percentage_red tablet:text-percentage_red'} mobile:text-white desktop:text-base tablet:text-base mobile:text-sm`}
>
{rightColumn?.line1?.price}
</Body>
)}
<div
className={`flex gap-1 items-center desktop:px-1 desktop:rounded tablet:px-1 tablet:rounded ${rightColumn?.line1?.direction === 'UP' && 'text-market_row_green desktop:bg-market_row_green/[.1] tablet:bg-market_row_green/[.1] mobile:bg-transparent'} ${rightColumn?.line1?.direction === 'DOWN' && 'text-percentage_red desktop:bg-percentage_red/[.1] tablet:bg-percentage_red/[.1] mobile:bg-transparent'}`}
>
{(rightColumn?.line1?.direction === 'UP' ||
rightColumn?.line1?.direction === 'DOWN') && (
<TbTriangleFilled
size={6}
color={
rightColumn?.line1?.direction === 'UP' ? '#5CFF93' : '#FF366C'
}
/>
)}
{rightColumn?.line1?.percentage && (
<BodySmall className="desktop:text-sm tablet:text-sm mobile:text-xs">
{rightColumn?.line1?.percentage}
</BodySmall>
)}
</div>
</div>
{rightColumn?.line2?.transactionCount && (
<BodySmall className="mobile:hidden text-white desktop:text-sm tablet:text-sm mobile:text-xs">
<span className="text-white/[.5]">Txs:</span>{' '}
{rightColumn?.line2?.transactionCount}
</BodySmall>
)}
</div>
);
};

export default RightColumnTokenMarketDataRow;
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [isBrokenImageChain, setIsBrokenImageChain] = useState<boolean>(false);

return (
<div className="relative w-10 h-10 mobile:w-9 mobile:h-9 rounded-full flex-shrink-0">
{tokenLogo && !isBrokenImage ? (
<img
src={tokenLogo}
alt="token-logo"
className="w-full h-full object-fill rounded-full"
data-testid="token-info-horizontal-logo"
onError={() => setIsBrokenImage(true)}
/>
) : (
<div className="w-full h-full overflow-hidden rounded-full">
<RandomAvatar name={tokenName || ''} />
</div>
)}

{/* Overlay text when no logo available */}
{(!tokenLogo || isBrokenImage) && (
<span className="absolute inset-0 flex items-center justify-center text-white text-base font-normal">
{tokenName?.slice(0, 2)}
</span>
)}

{/* Blockchain logo overlapping when only one blockchain for this token */}
{chainLogo && !isBrokenImageChain ? (
<div className="absolute bottom-0 right-0 w-4 h-4 mobile:w-3 mobile:h-3 rounded-full overflow-hidden border-[1px] bg-white border-container_grey transform translate-x-1/5 translate-y-1/5">
<img
src={chainLogo}
alt="logo"
className="w-full h-full object-contain"
onError={() => setIsBrokenImageChain(true)}
/>
</div>
) : null}
</div>
);
};

export default TokenLogoMarketDataRow;
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`flex w-full h-full items-center justify-between gap-2 py-3 border-b-[1px] border-[#25232D]
${isLastNumber && 'desktop:border-b-0 tablet:border-b-0 mobile:border-b-0'}
${isMiddleNumber && 'desktop:border-b-0'}
${data.link && 'cursor-pointer'}`}
onClick={() => (data.link ? navigate(`${data.link}`) : undefined)}
>
<div className="flex items-center flex-1 min-w-0">
<Body className="desktop:text-base tablet:text-base mobile:text-sm font-normal text-white/[0.5] mr-4 mobile:mr-2.5">
{listNumber > 0 && listNumber < 10 ? `0${listNumber}` : listNumber}
</Body>
<TokenLogoMarketDataRow
tokenLogo={data.leftColumn?.token?.primaryImage}
chainLogo={data.leftColumn?.token?.secondaryImage}
tokenName={data.leftColumn?.line1?.text2}
/>
<LeftColumnTokenMarketDataRow data={data} />
</div>
<div className="flex-shrink-0">
<RightColumnTokenMarketDataRow data={data} />
</div>
</div>
);
};

export default TokenMarketDataRow;
Original file line number Diff line number Diff line change
@@ -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('<LeftColumnTokenMarketDataRow /> - ETH token row', () => {
it('renders and matches snapshot', () => {
const tree = render(<LeftColumnTokenMarketDataRow data={ethTokenRow} />);
expect(tree).toMatchSnapshot();
});

it('renders text1 and text2', () => {
render(<LeftColumnTokenMarketDataRow data={ethTokenRow} />);
expect(screen.getByText('ETH')).toBeInTheDocument();
expect(screen.getByText('Ethereum')).toBeInTheDocument();
});

it('renders volume and liquidity', () => {
render(<LeftColumnTokenMarketDataRow data={ethTokenRow} />);
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(<LeftColumnTokenMarketDataRow data={incomplete} />);

expect(screen.getByText('ETH')).toBeInTheDocument();
expect(screen.queryByText(/Vol:/)).not.toBeInTheDocument();
expect(screen.queryByText(/Liq:/)).not.toBeInTheDocument();
});
});
Loading