Skip to content

Commit

Permalink
feat: NFTCard support address and tokenId (#177)
Browse files Browse the repository at this point in the history
* feat: NFTCard support address and tokenId

* fix: ts type for ci

* feat: support getNFTMetadata and add test case

---------

Co-authored-by: yutingzhao1991 <[email protected]>
  • Loading branch information
yutingzhao1991 and yutingzhao1991 authored Dec 5, 2023
1 parent 05ca67a commit 884a222
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 17 deletions.
3 changes: 2 additions & 1 deletion packages/common/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export function fillAddressWith0x(address: string): `0x${string}` {
return (address.startsWith('0x') ? address : `0x${address}`) as `0x${string}`;
}

export function parseNumberToBigint(num: number | bigint) {
export function parseNumberToBigint(num?: number | bigint) {
if (num === undefined) return undefined;
return typeof num !== 'bigint' ? BigInt(num) : num;
}
21 changes: 15 additions & 6 deletions packages/web3/src/hooks/useNFT.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { useEffect, useState } from 'react';
import type { NFTMetadata } from '@ant-design/web3-common';
import type { NFTMetadata, Web3ConfigProviderProps } from '@ant-design/web3-common';
import useProvider from './useProvider';

export default function useNFT(address: string, tokenId: bigint | number) {
export default function useNFT(
address?: string,
tokenId?: bigint | number,
getNFTMetadata?: Web3ConfigProviderProps['getNFTMetadata'],
) {
const [metadata, setMetadata] = useState<NFTMetadata>({});
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error>();
const { getNFTMetadata } = useProvider();
const { getNFTMetadata: getNFTMetadataFunc } = useProvider({
getNFTMetadata,
});

useEffect(() => {
if (getNFTMetadata) {
if (!address || !tokenId) {
return;
}
if (getNFTMetadataFunc) {
setLoading(true);
getNFTMetadata({
getNFTMetadataFunc({
address,
tokenId: BigInt(tokenId),
})
Expand All @@ -27,7 +36,7 @@ export default function useNFT(address: string, tokenId: bigint | number) {
} else {
setError(new Error('Provider is not ready'));
}
}, [address, tokenId, getNFTMetadata]);
}, [address, tokenId, getNFTMetadataFunc]);
return {
loading,
metadata,
Expand Down
28 changes: 21 additions & 7 deletions packages/web3/src/nft-card/NFTCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ import classNames from 'classnames';
import type { ImageProps } from 'antd';
import { Button, Divider, Image, ConfigProvider } from 'antd';
import Icon from '@ant-design/icons';
import useNFT from '../hooks/useNFT';
import { ReactComponent as ETHSvg } from './icons/eth.svg';
import { ReactComponent as HeartSvg } from './icons/heart.svg';
import { ReactComponent as HeartFilledSvg } from './icons/heart-filled.svg';
import useToken from 'antd/es/theme/useToken';
import { formatNumUnit, isDarkTheme } from '../utils/tool';
import {
parseNumberToBigint,
getWeb3AssetUrl,
type Web3ConfigProviderProps,
} from '@ant-design/web3-common';

const customizePrefixCls = 'ant-nft-card';

interface NFTCardProps {
address?: string;
tokenId?: number | bigint;
getNFTMetadata?: Web3ConfigProviderProps['getNFTMetadata'];
actionText?: ReactNode;
antdImageProps?: ImageProps;
className?: string;
description?: ReactNode;
image: string | ReactNode;
image?: string | ReactNode;
like?: {
liked?: boolean;
totalLikes?: number;
Expand All @@ -27,7 +36,6 @@ interface NFTCardProps {
price?: number;
footer?: ReactNode;
name?: string;
tokenId?: number;
style?: React.CSSProperties;
showAction?: boolean;
type?: 'default' | 'pithy';
Expand All @@ -38,19 +46,25 @@ const NFTCard: React.FC<NFTCardProps> = ({
style,
antdImageProps,
className,
description,
type = 'default',
image,
name,
address,
tokenId,
price = 0,
like: likeConfig,
showAction,
actionText = 'Buy Now',
footer,
getNFTMetadata,
...metadataProps
}) => {
const { liked, totalLikes = 0, onLikeChange } = likeConfig || {};
const [, token] = useToken();
const { metadata } = useNFT(address, parseNumberToBigint(tokenId), getNFTMetadata);
const {
name = metadata.name,
image = metadata.image,
description = metadata.description,
} = metadataProps;
const { getPrefixCls } = React.useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('nft-card', customizePrefixCls);
//================== Style ==================
Expand Down Expand Up @@ -120,14 +134,14 @@ const NFTCard: React.FC<NFTCardProps> = ({
<div className={`${prefixCls}-serial-number`}>{`#${tokenId}`}</div>
) : null}
{typeof image === 'string' ? (
<Image width="100%" src={image} {...antdImageProps} />
<Image width="100%" src={getWeb3AssetUrl(image)} {...antdImageProps} />
) : (
image
)}
</div>
<div className={`${prefixCls}-body`}>
{tokenId !== undefined && type === 'pithy' ? (
<div className={`${prefixCls}-serial-number`}>No:{tokenId}</div>
<div className={`${prefixCls}-serial-number`}>No:{tokenId.toString()}</div>
) : null}
{name ? <div className={`${prefixCls}-name`}>{name}</div> : null}
{description ? <div className={`${prefixCls}-description`}>{description}</div> : null}
Expand Down
43 changes: 43 additions & 0 deletions packages/web3/src/nft-card/__tests__/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NFTCard } from '@ant-design/web3';
import { render } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';

describe('NFTImage', () => {
it('renders correctly with valid address and tokenId', () => {
const address = '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B';
const tokenId = 123;

expect(() => render(<NFTCard address={address} tokenId={tokenId} />)).not.toThrow();
});

it('renders correctly with valid address and bigint tokenId', () => {
const address = '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B';
const tokenId = BigInt(123);

expect(() => render(<NFTCard address={address} tokenId={tokenId} />)).not.toThrow();
});

it('getNFTMetadata', async () => {
const address = '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B';
const tokenId = 123;

const { baseElement } = render(
<NFTCard
address={address}
tokenId={tokenId}
getNFTMetadata={async () => {
return {
name: 'NFT Name',
description: 'NFT Description',
image: 'ipfs://QmXVH2TsfCXJ5pDM3cabHKW1Z7M6fAtu5yV6LuifVWPsoP',
};
}}
/>,
);
await vi.waitFor(() => {
expect(baseElement.querySelector('.ant-image-img')?.getAttribute('src')).toBe(
'https://ipfs.io/ipfs/QmXVH2TsfCXJ5pDM3cabHKW1Z7M6fAtu5yV6LuifVWPsoP',
);
});
});
});
27 changes: 27 additions & 0 deletions packages/web3/src/nft-card/demos/wagmi.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createConfig, configureChains, mainnet } from 'wagmi';
import { infuraProvider } from 'wagmi/providers/infura';
import { WagmiWeb3ConfigProvider } from '@ant-design/web3-wagmi';
import { NFTCard } from '@ant-design/web3';

const { publicClient } = configureChains(
[mainnet],
[
infuraProvider({
apiKey: YOUR_INFURA_API_KEY,
}),
],
);

const config = createConfig({
publicClient,
});

const App: React.FC = () => {
return (
<WagmiWeb3ConfigProvider config={config}>
<NFTCard address="0x79fcdef22feed20eddacbb2587640e45491b757f" tokenId={42n} />
</WagmiWeb3ConfigProvider>
);
};

export default App;
12 changes: 10 additions & 2 deletions packages/web3/src/nft-card/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,33 @@ Components used to display NFTCard.

<code src="./demos/dark-mode.tsx"></code>

## Use with wagmi

<code src="./demos/wagmi.tsx"></code>

## API

| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| address | The address of the NFT | `string` | - | - |
| tokenId | The tokenId of the NFT | `number \| bigint` | - | - |
| getNFTMetadata | The method to get the metadata of the NFT | `(address: string, tokenId: number \| bigint) => Promise<NFTMetadata>` | - | - |
| actionText | The text of the main button in the card | `React.ReactNode` | 'Buy Now' | - |
| antdImageProps | The props of antd Image component | [ImageProps](https://ant-design.antgroup.com/components/image#api)| - | - |
| antdImageProps | The props of antd Image component | [ImageProps](https://ant-design.antgroup.com/components/image#api) | - | - |
| className | The className of the card | `string` | - | - |
| description | The description of the card | `React.ReactNode` | - | - |
| image | The image of the card | `string \| React.ReactNode` | - | - |
| like | The props of like | [LikeProps](#likeprops) | - | - |
| price | The price of the card | `number` | `0` | - |
| footer | The footer of the card | `React.ReactNode` | - | - |
| name | The name of the card | `string` | - | - |
| tokenId | The tokenId of the NFT | `number` | - | - |
| style | The style of the card | `React.CSSProperties` | - | - |
| showAction | Whether to show the main button of the card | `boolean` | `true` | - |
| type | The type of the card | `'default' \| 'pithy'` | `'default'` | - |
| onActionChange | The callback when the main button of the card is clicked | `() => void` | - | - |

The definition of `NFTMetadata` refers to the Ethereum ERC721 standard, see [NFTMetadata document](../types/index.md#nftmetadata) for details.

### LikeProps

| Property | Description | Type | Default | Version |
Expand Down
10 changes: 9 additions & 1 deletion packages/web3/src/nft-card/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ group: 展示

<code src="./demos/dark-mode.tsx"></code>

## 和 wagmi 一起使用

<code src="./demos/wagmi.tsx"></code>

## API

| 属性 | 描述 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| address | NFT 的地址 | `string` | - | - |
| tokenId | NFT 的 tokenId | `number \| bigint` | - | - |
| getNFTMetadata | 获取 NFT 元数据的方法 | `(address: string, tokenId: number \| bigint) => Promise<NFTMetadata>` | - | - |
| actionText | 卡片中主要按钮文案 | `React.ReactNode` | 'Buy Now' | - |
| antdImageProps | antd Image 组件的 props | [ImageProps](https://ant-design.antgroup.com/components/image#api) | - | - |
| className | 卡片的类名 | `string` | - | - |
Expand All @@ -28,12 +35,13 @@ group: 展示
| price | 卡片的价格 | `number` | `0` | - |
| footer | 卡片的底部内容 | `React.ReactNode` | - | - |
| name | 卡片的名称 | `string` | - | - |
| tokenId | NFT 的 tokenId | `number` | - | - |
| style | 卡片的样式 | `React.CSSProperties` | - | - |
| showAction | 是否显示卡片的主要按钮 | `boolean` | `true` | - |
| type | 卡片的类型 | `'default' \| 'pithy'` | `'default'` | - |
| onActionChange | 点击卡片的主要按钮时的回调 | `() => void` | - | - |

`NFTMetadata` 的定义参考以太坊 ERC721 的标准,具体见 [NFTMetadata 文档](../types/index.zh-CN.md#nftmetadata)

### LikeProps

| 属性 | 描述 | 类型 | 默认值 | 版本 |
Expand Down

0 comments on commit 884a222

Please sign in to comment.