Commit bd94c0ee authored by tom's avatar tom

token entity implementation

parent 93099d26
...@@ -14,6 +14,7 @@ import { getIconProps, type IconSize } from './utils'; ...@@ -14,6 +14,7 @@ import { getIconProps, type IconSize } from './utils';
export type Truncation = 'constant' | 'dynamic' | 'tail' | 'none'; export type Truncation = 'constant' | 'dynamic' | 'tail' | 'none';
// TODO @tom2drum add disabled link props
export interface EntityBaseProps { export interface EntityBaseProps {
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
...@@ -23,6 +24,7 @@ export interface EntityBaseProps { ...@@ -23,6 +24,7 @@ export interface EntityBaseProps {
href?: string; href?: string;
query?: Record<string, string>; query?: Record<string, string>;
noIcon?: boolean; noIcon?: boolean;
// TODO @tom2drum rename to noCopy
withCopy?: boolean; withCopy?: boolean;
tailLength?: number; tailLength?: number;
truncation?: Truncation; truncation?: Truncation;
...@@ -32,7 +34,7 @@ export interface ContainerBaseProps extends Pick<EntityBaseProps, 'className'> { ...@@ -32,7 +34,7 @@ export interface ContainerBaseProps extends Pick<EntityBaseProps, 'className'> {
children: React.ReactNode; children: React.ReactNode;
} }
const Container = ({ className, children }: ContainerBaseProps) => { const Container = chakra(({ className, children }: ContainerBaseProps) => {
return ( return (
<Flex <Flex
className={ className } className={ className }
...@@ -42,7 +44,7 @@ const Container = ({ className, children }: ContainerBaseProps) => { ...@@ -42,7 +44,7 @@ const Container = ({ className, children }: ContainerBaseProps) => {
{ children } { children }
</Flex> </Flex>
); );
}; });
export interface LinkBaseProps extends Pick<EntityBaseProps, 'className' | 'onClick' | 'isLoading' | 'isExternal' | 'href'> { export interface LinkBaseProps extends Pick<EntityBaseProps, 'className' | 'onClick' | 'isLoading' | 'isExternal' | 'href'> {
children: React.ReactNode; children: React.ReactNode;
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as tokenMock from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import TokenEntity from './TokenEntity';
const iconSizes = [ 'md', 'lg' ];
test.use({ viewport: { width: 300, height: 100 } });
test.describe('icon size', () => {
iconSizes.forEach((size) => {
test(size, async({ mount }) => {
const component = await mount(
<TestApp>
<TokenEntity
token={ tokenMock.tokenInfo }
iconSize={ size }
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
});
});
test('with logo, long name and symbol', async({ page, mount }) => {
const LOGO_URL = 'https://example.com/logo.png';
await page.route(LOGO_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
await mount(
<TestApp>
<TokenEntity
token={{
name: 'This token is the best token ever',
symbol: 'DUCK DUCK DUCK',
address: tokenMock.tokenInfo.address,
icon_url: LOGO_URL,
}}
/>
</TestApp>,
);
await page.getByText(/this/i).hover();
await expect(page).toHaveScreenshot();
await page.getByText(/duc/i).hover();
await expect(page).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenEntity
token={ tokenMock.tokenInfo }
isLoading
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('customization', async({ mount }) => {
const component = await mount(
<TestApp>
<Box
borderWidth="1px"
borderColor="orange.500"
>
<TokenEntity
token={ tokenMock.tokenInfo }
p={ 2 }
maxW="200px"
borderWidth="1px"
borderColor="blue.700"
/>
</Box>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import type { As } from '@chakra-ui/react';
import { Image, Skeleton, chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import { getIconProps } from '../base/utils';
interface LinkProps extends Pick<EntityProps, 'className' | 'token' | 'onClick' | 'isLoading' | 'isExternal' | 'href' | 'query'> {
children: React.ReactNode;
}
const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/token/[hash]', query: { ...props.query, hash: props.token.address } });
return (
<EntityBase.Link
{ ...props }
href={ props.href ?? defaultHref }
>
{ props.children }
</EntityBase.Link>
);
});
type IconProps = Pick<EntityProps, 'token' | 'isLoading' | 'iconSize' | 'noIcon'> & {
asProp?: As;
};
const Icon = (props: IconProps) => {
if (props.noIcon) {
return null;
}
const styles = {
...getIconProps(props.iconSize),
marginRight: 2,
borderRadius: 'base',
};
if (props.isLoading) {
return <Skeleton { ...styles } flexShrink={ 0 }/>;
}
return (
<Image
{ ...styles }
src={ props.token.icon_url ?? undefined }
alt={ `${ props.token.name || 'token' } logo` }
fallback={ <TokenLogoPlaceholder { ...styles }/> }
/>
);
};
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'token'>;
const Content = chakra((props: ContentProps) => {
const name = props.token.name ?? 'Unnamed token';
return (
<TruncatedTextTooltip label={ name }>
<Skeleton
isLoaded={ !props.isLoading }
display="inline-block"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
height="fit-content"
>
{ name }
</Skeleton>
</TruncatedTextTooltip>
);
});
type SymbolProps = Pick<EntityProps, 'token' | 'isLoading' | 'noSymbol'>;
const Symbol = (props: SymbolProps) => {
const symbol = props.token.symbol;
if (!symbol || props.noSymbol) {
return null;
}
return (
<Skeleton
isLoaded={ !props.isLoading }
display="inline-flex"
alignItems="center"
maxW="20%"
ml={ 2 }
color="text_secondary"
>
<div>(</div>
<TruncatedTextTooltip label={ symbol }>
<chakra.span
display="inline-block"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
height="fit-content"
>
{ symbol }
</chakra.span>
</TruncatedTextTooltip>
<div>)</div>
</Skeleton>
);
};
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'token'>;
const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...props }
text={ props.token.address }
withCopy={ props.withCopy ?? true }
/>
);
};
const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps {
token: Pick<TokenInfo, 'address' | 'icon_url' | 'name' | 'symbol'>;
noSymbol?: boolean;
}
const TokenEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
return (
<Container className={ props.className } w="100%">
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
</Link>
<Symbol { ...partsProps }/>
<Copy { ...partsProps }/>
</Container>
);
};
export default React.memo(chakra(TokenEntity));
export {
Container,
Link,
Icon,
Content,
Copy,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment