Skip to content

Commit

Permalink
Feature: APP-2445 - ActionItemAddress (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabricevladimir authored Sep 8, 2023
1 parent d187275 commit bb773c6
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `ActionItemAddress` component

## [0.2.13] - 2023-08-31

### Fixed
Expand Down
65 changes: 65 additions & 0 deletions src/components/actionItems/actionItemAddress.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Button.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { IconCopy, IconGovernance, IconLinkExternal } from '../icons';
import { ListItemAction } from '../listItem';
import { ActionItemAddress, TAG_WALLET_ID_VARIANTS } from './actionItemAddress';

const meta: Meta<typeof ActionItemAddress> = {
component: ActionItemAddress,
title: 'Components/ActionItems/Address',
tags: ['autodocs'],
argTypes: {
addressOrEns: { control: { type: 'text' } },
avatar: { control: { type: 'text' } },
tagLabel: { control: { type: 'text' } },
walletId: {
options: TAG_WALLET_ID_VARIANTS,
control: { type: 'radio' },
},
},
};

export default meta;

type Story = StoryObj<typeof ActionItemAddress>;

export const Primary: Story = {
render: ({ ...args }) => <ActionItemAddress {...args} />,
args: {
addressOrEns: '0x0000000000000000000000000000000000000000',
tagLabel: 'You',
delegations: 2,
delegationLabel: 'Delegations',
supplyPercentage: 10,
tokenAmount: '1000',
tokenSymbol: 'ANT',
menuOptions: [
{
component: (
<ListItemAction title="Copy Address" iconRight={<IconCopy className="text-ui-300" />} bgWhite />
),
},
{
component: (
<ListItemAction
title="View on block explorer"
iconRight={<IconLinkExternal className="text-ui-300" />}
bgWhite
/>
),
},
{
component: (
<ListItemAction
title="Delegate to"
iconRight={<IconGovernance className="text-ui-300" />}
bgWhite
/>
),
},
],
},
};
176 changes: 176 additions & 0 deletions src/components/actionItems/actionItemAddress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React from 'react';
import styled from 'styled-components';

import { useScreen } from '../../hooks';
import { shortenAddress } from '../../utils';
import { Avatar } from '../avatar';
import { ButtonIcon } from '../button';
import { Dropdown, type ListItemProps } from '../dropdown';
import { IconLinkExternal, IconMenuVertical } from '../icons';
import { Tag } from '../tag';

/**
* Type declarations for `ActionItemAddressProps`.
*/
export type ActionItemAddressProps = {
/** Wallet address or ENS domain name. */
addressOrEns: string;

/** Optional ENS avatar URL. If not provided and the wallet address is valid,
* it will be used to generate a Blockies avatar.
*/
avatar?: string;

/** Label for the delegation amount. */
delegationLabel: string;

/** Number of delegations. */
delegations: number;

/** List of dropdown menu options. */
menuOptions: ListItemProps[];

/** Optional click handler for the "view on-chain" action. */
onViewOnChainClick?: React.MouseEventHandler;

/** Percentage of supply. */
supplyPercentage: number;

/** Optional label for the wallet tag. */
tagLabel?: string;

/** Number of tokens delegated. */
tokenAmount: string;

/** Symbol of the token delegated. */
tokenSymbol: string;

/** ID variant for the wallet, which can be 'delegate' or 'you'. */
walletId?: TagWalletIdProps['variant'];
};

/**
* `ActionItemAddress` component: Displays an address item with associated actions.
* @param props - Component properties following `ActionItemAddressProps` type.
* @returns JSX Element.
*/
export const ActionItemAddress: React.FC<ActionItemAddressProps> = (props) => {
const {
addressOrEns,
avatar,
delegationLabel,
delegations,
menuOptions,
onViewOnChainClick,
supplyPercentage,
tagLabel,
tokenAmount,
tokenSymbol,
walletId,
} = props;

const { isDesktop } = useScreen();

return (
<Container>
<Avatar size="small" mode="circle" src={avatar ?? addressOrEns} />

<ContentWrapper>
<Wallet>
<AddressOrEns>{shortenAddress(addressOrEns)}</AddressOrEns>
{walletId && tagLabel && (
<TagWalletId
label={tagLabel}
variant={walletId}
className="inline-flex relative -top-0.5 -right-0.5"
/>
)}
</Wallet>
<Content>
<InfoWrapper>
<span>{tokenAmount}</span>
<span>{tokenSymbol}</span>
<InfoLabel>{supplyPercentage}%</InfoLabel>
</InfoWrapper>
<InfoWrapper>
{delegations > 0 && (
<>
<span>{delegations}</span>
<InfoLabel>{delegationLabel}</InfoLabel>
</>
)}
</InfoWrapper>
</Content>
</ContentWrapper>
<ButtonGroup>
{isDesktop && (
<ButtonIcon
mode="ghost"
icon={<IconLinkExternal />}
size="small"
bgWhite
onClick={onViewOnChainClick}
/>
)}

<Dropdown
align="end"
alignOffset={isDesktop ? 0 : -4}
className="py-1 px-0"
listItems={menuOptions}
side="top"
sideOffset={isDesktop ? -40 : -44}
trigger={<ButtonIcon mode="secondary" icon={<IconMenuVertical />} size="small" bgWhite />}
/>
</ButtonGroup>
</Container>
);
};

export const TAG_WALLET_ID_VARIANTS = ['delegate', 'you'] as const;
type TagWalletIdVariant = (typeof TAG_WALLET_ID_VARIANTS)[number];

/**
* Type declarations for `TagWalletIdProps`.
*/
type TagWalletIdProps = {
/** Optional CSS classes to apply to the tag. */
className?: string;
/** Label to display on the tag. */
label: string;
/** Variant of the tag which affects its color. Can be 'delegate' or 'you'. */
variant: TagWalletIdVariant;
};

/**
* `TagWalletId` component: Displays a styled tag based on the provided variant.
* @param props - Component properties following `TagWalletIdProps` type.
* @returns JSX Element.
*/
const TagWalletId: React.FC<TagWalletIdProps> = ({ className, label, variant }) => {
const colorScheme = variant === 'you' ? 'neutral' : 'info';
return <Tag label={label} colorScheme={colorScheme} className={className} />;
};

const Container = styled.div.attrs({
className:
'bg-ui-0 flex py-2 items-center px-1.5 desktop:pr-2 desktop:pl-3 space-x-2 w-full border-b border-b-ui-100',
})``;

const ContentWrapper = styled.div.attrs({ className: 'desktop:flex flex-1 space-y-0.5' })``;

const Content = styled.div.attrs({
className: 'flex desktop:flex-1 space-x-1.5 font-semibold text-ui-600 ft-text-sm',
})``;

const InfoWrapper = styled.div.attrs({
className: 'flex desktop:flex-1 space-x-0.25',
})``;

const InfoLabel = styled.span.attrs({ className: 'font-normal text-ui-500' })``;

const Wallet = styled.div.attrs({ className: 'flex desktop:flex-1 items-center' })``;

const AddressOrEns = styled.div.attrs({ className: 'font-semibold text-ui-800 ft-text-base' })``;

const ButtonGroup = styled.div.attrs({ className: 'flex gap-x-1.5' })``;
1 change: 1 addition & 0 deletions src/components/actionItems/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './actionItemAddress';
40 changes: 29 additions & 11 deletions src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import Blockies from 'react-blockies';
import styled from 'styled-components';

import { IsAddress } from '../../utils/addresses';

export type AvatarProps = {
Expand All @@ -23,15 +24,30 @@ const sizes: SizesType = {

/** Simple Avatar*/
export const Avatar: React.FC<AvatarProps> = ({ mode = 'circle', size = 'default', src }) => {
return (
<AvatarContainer mode={mode} size={size}>
{IsAddress(src) ? (
<Blockies seed={src} size={BLOCKIES_SQUARES} scale={sizes[size].scale} />
) : (
<StyledAvatar {...{ mode, size, src }} />
)}
</AvatarContainer>
);
const [error, setError] = useState(false);

useEffect(() => {
setError(false);
}, [src]);

if (!error) {
return (
<AvatarContainer mode={mode} size={size}>
{IsAddress(src) || src?.endsWith('.eth') ? (
<Blockies seed={src} size={BLOCKIES_SQUARES} scale={sizes[size].scale} />
) : (
<StyledAvatar
{...{ mode, size, src }}
onError={() => {
setError(true);
}}
/>
)}
</AvatarContainer>
);
}

return <FallbackAvatar />;
};

type StyledAvatarProps = Pick<AvatarProps, 'size'>;
Expand All @@ -46,6 +62,8 @@ const AvatarContainer = styled.div.attrs(({ size, mode }: StyledContainerProps)
const className = `overflow-hidden bg-ui-100
${sizes[size].sizes}
${mode === 'circle' ? 'rounded-full' : 'rounded-2xl'}
`;
`;
return { className };
})<StyledContainerProps>``;

const FallbackAvatar = styled.div.attrs({ className: 'w-3 h-3 rounded-full bg-gradient-to-tl from-ui-900' })``;
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './actionItems';
export * from './alerts';
export * from './avatar';
export * from './backdrop';
Expand Down

0 comments on commit bb773c6

Please sign in to comment.