-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat: draft for erc721 card #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { NFTCard as NFTCard$ } from "@geist/ui-react/components/nft/nft-card"; | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { withWagmiProvider } from "#stories/decorators/wagmi.tsx"; | ||
|
|
||
| interface ERC721CardStoriesProps { | ||
| tokenId: string; | ||
| contractAddress: string; | ||
| } | ||
|
|
||
| function ERC721CardStories({ | ||
| tokenId, | ||
| contractAddress, | ||
| }: ERC721CardStoriesProps) { | ||
| return ( | ||
| <NFTCard$ | ||
| tokenId={tokenId} | ||
| contractAddress={contractAddress} | ||
| className="max-w-96 min-w-40" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const meta = { | ||
| title: "NFT/ERC721Card", | ||
| component: ERC721CardStories, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| decorators: [withWagmiProvider()], | ||
| } satisfies Meta<typeof ERC721CardStories>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const ERC721Card: Story = { | ||
| args: { | ||
| tokenId: "6273", | ||
| contractAddress: "0xbd3531da5cf5857e7cfaa92426877b022e612cf8", | ||
| }, | ||
| parameters: {}, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { CardContent } from "#components/shadcn/card.js"; | ||
| import { cn } from "#lib/shadcn/utils.js"; | ||
| import React from "react"; | ||
|
|
||
| export const NFTContent = React.forwardRef< | ||
| HTMLDivElement, | ||
| React.HTMLAttributes<HTMLDivElement> | ||
| >(({ className, ...props }, ref) => ( | ||
| <CardContent | ||
| ref={ref} | ||
| className={cn("pt-6 space-y-2", className)} | ||
| {...props} | ||
| /> | ||
| )); | ||
|
|
||
| NFTContent.displayName = "NFTContent"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { useNFT } from "./provider"; | ||
|
|
||
| export function NFTMedia() { | ||
| const { nftData } = useNFT(); | ||
|
|
||
| const imageUrl = nftData?.image_url; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iyansr do you hv example data for blockscout nft url? (reference |
||
|
|
||
| if (!imageUrl) { | ||
| return <div className="aspect-square bg-neutral-200" />; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="aspect-square"> | ||
| <img | ||
| src={imageUrl} | ||
| alt={nftData?.metadata?.name ?? ""} | ||
| className="object-cover h-full w-full" | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { CardDescription, CardTitle } from "#components/shadcn/card.js"; | ||
| import { cn } from "#lib/shadcn/utils.js"; | ||
| import React from "react"; | ||
| import { useNFT } from "./provider"; | ||
| import { formatAddress } from "../utils"; | ||
|
|
||
| export const NFTOwner = React.forwardRef< | ||
| HTMLDivElement, | ||
| React.HTMLAttributes<HTMLDivElement> | ||
| >(({ className, ...props }, ref) => { | ||
| const { nftData } = useNFT(); | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn("flex items-center justify-between", className)} | ||
| {...props} | ||
| > | ||
| <CardDescription ref={ref}>Owned by</CardDescription> | ||
| <CardDescription | ||
| ref={ref} | ||
| className="text-right font-semibold text-black" | ||
| > | ||
| {nftData?.owner?.ens_domain_name ?? | ||
| formatAddress(nftData?.owner?.hash ?? "")} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets use existing components / utils |
||
| </CardDescription> | ||
| </div> | ||
| ); | ||
| }); | ||
|
|
||
| NFTOwner.displayName = "NFTOwner"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { CardTitle } from "#components/shadcn/card.js"; | ||
| import { cn } from "#lib/shadcn/utils.js"; | ||
| import React from "react"; | ||
| import { useNFT } from "./provider"; | ||
|
|
||
| export const NFTTitle = React.forwardRef< | ||
| HTMLDivElement, | ||
| React.HTMLAttributes<HTMLDivElement> | ||
| >(({ className, ...props }, ref) => { | ||
| const { nftData } = useNFT(); | ||
|
|
||
| return ( | ||
| <CardTitle ref={ref} className={cn(className)} {...props}> | ||
| {nftData?.metadata?.name} | ||
| </CardTitle> | ||
| ); | ||
| }); | ||
|
|
||
| NFTTitle.displayName = "NFTTitle"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { | ||
| createContext, | ||
| useContext, | ||
| useMemo, | ||
| type PropsWithChildren, | ||
| } from "react"; | ||
| import type { Address } from "viem"; | ||
| import type { NFTCardType } from "../types"; | ||
| import { useGetSingleNFTMetadata } from "#hooks/data/use-blockscout.js"; | ||
| import type { NFTResponse } from "#lib/blockscout/types.js"; | ||
|
|
||
| export type NFTContextType = { | ||
| nftData: NFTResponse | undefined; | ||
| loading: boolean; | ||
| error: Error | null; | ||
| }; | ||
|
|
||
| export const NFTContext = createContext<NFTContextType>(undefined as never); | ||
|
|
||
| export const NFTProvider = ({ | ||
| children, | ||
| contractAddress, | ||
| tokenId, | ||
| }: PropsWithChildren<NFTCardType>) => { | ||
| const { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in this case it's fine while generally we might prefer nanostore over context provider |
||
| data: nftData, | ||
| isLoading: loading, | ||
| error, | ||
| } = useGetSingleNFTMetadata(contractAddress, tokenId); | ||
|
|
||
| const value = useMemo( | ||
| () => ({ nftData: nftData, loading, error }), | ||
| [nftData, loading, error], | ||
| ); | ||
|
|
||
| return <NFTContext.Provider value={value}>{children}</NFTContext.Provider>; | ||
| }; | ||
|
|
||
| export const useNFT = () => { | ||
| const context = useContext(NFTContext); | ||
| if (!context) { | ||
| throw new Error("useNFT must be used within a NFTProvider"); | ||
| } | ||
| return context; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { Card } from "#components/shadcn/card.js"; | ||
| import { cn } from "#lib/shadcn/utils.js"; | ||
| import React, { type HTMLAttributes, type PropsWithChildren } from "react"; | ||
| import { NFTProvider } from "./components/provider"; | ||
| import type { Address } from "viem"; | ||
| import { NFTMedia } from "./components/nft-media"; | ||
| import { NFTTitle } from "./components/nft-title"; | ||
| import { NFTContent } from "./components/nft-content"; | ||
| import { NFTOwner } from "./components/nft-owner"; | ||
|
|
||
| function NFTCardDefaultContent() { | ||
| return ( | ||
| <> | ||
| <NFTMedia /> | ||
| <NFTContent> | ||
| <NFTTitle /> | ||
| <NFTOwner /> | ||
| </NFTContent> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| interface NFTCardProps extends HTMLAttributes<HTMLDivElement> { | ||
| tokenId: string; | ||
| contractAddress: string; | ||
| } | ||
|
|
||
| export const NFTCard = React.forwardRef< | ||
| HTMLDivElement, | ||
| PropsWithChildren<NFTCardProps> | ||
| >( | ||
| ( | ||
| { | ||
| tokenId, | ||
| contractAddress, | ||
| children = <NFTCardDefaultContent />, | ||
| className, | ||
| ...props | ||
| }, | ||
| ref, | ||
| ) => { | ||
| return ( | ||
| <NFTProvider | ||
| contractAddress={contractAddress as Address} | ||
| tokenId={tokenId} | ||
| > | ||
| <Card | ||
| ref={ref} | ||
| className={cn( | ||
| "flex w-full max-w-[500px] flex-col items-stretch gap-1.5 border overflow-hidden text-left", | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| {children} | ||
| </Card> | ||
| </NFTProvider> | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| NFTCard.displayName = "NFTCard"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import type { Address } from "viem"; | ||
|
|
||
| export type NFTCardType = { | ||
| contractAddress: Address; | ||
| tokenId: string; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const formatAddress = (address: string) => { | ||
| return address.slice(0, 6) + "..." + address.slice(-4); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| export interface NFTResponse { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can put types into the |
||
| is_unique: boolean; | ||
| id: string; | ||
| holder_address_hash?: string; | ||
| image_url?: string; | ||
| animation_url?: string; | ||
| external_app_url?: string; | ||
| metadata?: NFTResponseMetadata; | ||
| owner: Owner; | ||
| token: Token; | ||
| } | ||
|
|
||
| export interface NFTResponseMetadata { | ||
| year?: number; | ||
| tags?: string[]; | ||
| name?: string; | ||
| image_url?: string; | ||
| home_url?: string; | ||
| external_url?: string; | ||
| description?: string; | ||
| attributes?: Attribute[]; | ||
| } | ||
|
|
||
| export interface Attribute { | ||
| value: string; | ||
| trait_type: string; | ||
| } | ||
|
|
||
| export interface Owner { | ||
| hash: string; | ||
| implementation_name: string; | ||
| name: string; | ||
| ens_domain_name?: string; | ||
| metadata?: OwnerMetadata; | ||
| is_contract: boolean; | ||
| private_tags: Tag[]; | ||
| watchlist_names: WatchlistName[]; | ||
| public_tags: Tag[]; | ||
| is_verified: boolean; | ||
| } | ||
|
|
||
| export interface OwnerMetadata { | ||
| slug: string; | ||
| name: string; | ||
| tagType: string; | ||
| ordinal: number; | ||
| meta: Meta; | ||
| } | ||
|
|
||
| export interface Meta {} | ||
|
|
||
| export interface Tag { | ||
| address_hash: string; | ||
| display_name: string; | ||
| label: string; | ||
| } | ||
|
|
||
| export interface WatchlistName { | ||
| display_name: string; | ||
| label: string; | ||
| } | ||
|
|
||
| export interface Token { | ||
| circulating_market_cap: string; | ||
| icon_url: string; | ||
| name: string; | ||
| decimals: string; | ||
| symbol: string; | ||
| address: string; | ||
| type: string; | ||
| holders: string; | ||
| exchange_rate: string; | ||
| total_supply: string; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we create 2 stories for 2 separate smart vs dumb components
(similar to
TransactionTablevsTransactionTableWithDetailsend goal is we don't dictate the data source