-
Notifications
You must be signed in to change notification settings - Fork 619
[MNY-252] Dashboard: Update WalletAddress component UI #8276
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
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,120 @@ | ||
| import type { Meta, StoryObj } from "@storybook/nextjs"; | ||
| import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; | ||
| import type { SocialProfile } from "thirdweb/react"; | ||
| import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils"; | ||
| import { WalletAddress, WalletAddressUI } from "./wallet-address"; | ||
|
|
||
| const meta = { | ||
| component: Story, | ||
| parameters: { | ||
| nextjs: { | ||
| appDirectory: true, | ||
| }, | ||
| }, | ||
| title: "blocks/WalletAddress", | ||
| } satisfies Meta<typeof Story>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Variants: Story = { | ||
| args: {}, | ||
| }; | ||
|
|
||
| function Story() { | ||
| const client = storybookThirdwebClient as unknown as ThirdwebClient; | ||
|
|
||
| return ( | ||
| <div className="container flex max-w-4xl flex-col gap-8 py-10"> | ||
| <BadgeContainer label="Social Profiles Loaded"> | ||
| <WalletAddressUI | ||
| address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" | ||
| client={client} | ||
| profiles={{ | ||
| data: vitalikEth, | ||
| isPending: false, | ||
| }} | ||
| /> | ||
| </BadgeContainer> | ||
|
|
||
| <BadgeContainer label="No Social Profiles"> | ||
| <WalletAddressUI | ||
| address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425" | ||
| client={client} | ||
| shortenAddress | ||
| profiles={{ | ||
| data: [], | ||
| isPending: false, | ||
| }} | ||
| /> | ||
| </BadgeContainer> | ||
|
|
||
| <BadgeContainer label="Loading Social Profiles"> | ||
| <WalletAddressUI | ||
| address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425" | ||
| client={client} | ||
| shortenAddress | ||
| profiles={{ | ||
| data: [], | ||
| isPending: true, | ||
| }} | ||
| /> | ||
| </BadgeContainer> | ||
|
|
||
| <BadgeContainer label="Zero address"> | ||
| <WalletAddressUI | ||
| address={ZERO_ADDRESS} | ||
| client={client} | ||
| profiles={{ | ||
| data: [], | ||
| isPending: false, | ||
| }} | ||
| /> | ||
| </BadgeContainer> | ||
|
|
||
| <BadgeContainer label="Real Component - Invalid Address"> | ||
| <WalletAddress address="not-an-address" client={client} /> | ||
| </BadgeContainer> | ||
|
|
||
| <BadgeContainer label="Real Component - vitalik.eth"> | ||
| <WalletAddress | ||
| address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" | ||
| client={client} | ||
| /> | ||
| </BadgeContainer> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const vitalikEth: SocialProfile[] = [ | ||
| { | ||
| type: "ens", | ||
| name: "vitalik.eth", | ||
| bio: "mi pinxe lo crino tcati", | ||
| avatar: "https://euc.li/vitalik.eth", | ||
| metadata: { | ||
| name: "vitalik.eth", | ||
| address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", | ||
| avatar: "https://euc.li/vitalik.eth", | ||
| description: "mi pinxe lo crino tcati", | ||
| url: "https://vitalik.ca", | ||
| }, | ||
| }, | ||
| { | ||
| type: "farcaster", | ||
| name: "vitalik.eth", | ||
| bio: undefined, | ||
| avatar: | ||
| "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original", | ||
| metadata: { | ||
| fid: 5650, | ||
| bio: undefined, | ||
| pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original", | ||
| username: "vitalik.eth", | ||
| addresses: [ | ||
| "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", | ||
| "0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279", | ||
| ], | ||
| }, | ||
| }, | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,60 @@ | ||
| "use client"; | ||
| import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react"; | ||
| import { CircleSlashIcon, XIcon } from "lucide-react"; | ||
| import { useMemo } from "react"; | ||
| import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; | ||
| import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react"; | ||
| import { Img } from "@/components/blocks/Img"; | ||
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||
| import { Badge } from "@/components/ui/badge"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { | ||
| HoverCard, | ||
| HoverCardContent, | ||
| HoverCardTrigger, | ||
| } from "@/components/ui/hover-card"; | ||
| import { ToolTipLabel } from "@/components/ui/tooltip"; | ||
| import { useClipboard } from "@/hooks/useClipboard"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; | ||
| import { CopyTextButton } from "../ui/CopyTextButton"; | ||
| import { Skeleton } from "../ui/skeleton"; | ||
|
|
||
| export function WalletAddress(props: { | ||
| type WalletAddressProps = { | ||
| address: string | undefined; | ||
| shortenAddress?: boolean; | ||
| className?: string; | ||
| iconClassName?: string; | ||
| client: ThirdwebClient; | ||
| fallbackIcon?: React.ReactNode; | ||
| }) { | ||
| }; | ||
MananTank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export function WalletAddress(props: WalletAddressProps) { | ||
|
Contributor
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. 🛠️ Refactor suggestion | 🟠 Major Add explicit return types to exported functions. Both Apply this diff: -export function WalletAddress(props: WalletAddressProps) {
+export function WalletAddress(props: WalletAddressProps): JSX.Element { export function WalletAddressUI(
props: WalletAddressProps & {
profiles: {
data: SocialProfile[];
isPending: boolean;
};
},
-) {
+): JSX.Element {As per coding guidelines. Also applies to: 46-46 🤖 Prompt for AI Agents |
||
| const profiles = useSocialProfiles({ | ||
| address: props.address, | ||
| client: props.client, | ||
| }); | ||
|
|
||
| return ( | ||
| <WalletAddressUI | ||
| {...props} | ||
| profiles={{ | ||
| data: profiles.data || [], | ||
| isPending: profiles.isPending, | ||
| }} | ||
| /> | ||
| ); | ||
MananTank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| export function WalletAddressUI( | ||
| props: WalletAddressProps & { | ||
| profiles: { | ||
| data: SocialProfile[]; | ||
| isPending: boolean; | ||
| }; | ||
| }, | ||
| ) { | ||
| // default back to zero address if no address provided | ||
| const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]); | ||
|
|
||
| const [shortenedAddress, lessShortenedAddress] = useMemo(() => { | ||
| const [shortenedAddress, _lessShortenedAddress] = useMemo(() => { | ||
| return [ | ||
| props.shortenAddress !== false | ||
| ? `${address.slice(0, 6)}...${address.slice(-4)}` | ||
|
|
@@ -37,17 +63,10 @@ export function WalletAddress(props: { | |
| ]; | ||
| }, [address, props.shortenAddress]); | ||
|
|
||
| const profiles = useSocialProfiles({ | ||
| address: address, | ||
| client: props.client, | ||
| }); | ||
|
|
||
| const { onCopy, hasCopied } = useClipboard(address, 2000); | ||
|
|
||
| if (!isAddress(address)) { | ||
| return ( | ||
| <ToolTipLabel hoverable label={address}> | ||
| <span className="flex items-center gap-2 underline-offset-4 hover:underline"> | ||
| <span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit"> | ||
| <div className="flex size-5 items-center justify-center rounded-full border bg-background"> | ||
| <XIcon className="size-4 text-muted-foreground" /> | ||
| </div> | ||
|
|
@@ -88,89 +107,82 @@ export function WalletAddress(props: { | |
| <WalletAvatar | ||
| address={address} | ||
| iconClassName={props.iconClassName} | ||
| profiles={profiles.data || []} | ||
| profiles={props.profiles.data} | ||
| thirdwebClient={props.client} | ||
| fallbackIcon={props.fallbackIcon} | ||
| /> | ||
| )} | ||
| <span className="cursor-pointer font-mono max-w-full truncate"> | ||
| {profiles.data?.[0]?.name || shortenedAddress} | ||
| {props.profiles.data?.[0]?.name || shortenedAddress} | ||
| </span> | ||
| </Button> | ||
| </HoverCardTrigger> | ||
| <HoverCardContent | ||
| className="w-80 border-border" | ||
| className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6" | ||
| onClick={(e) => { | ||
| // do not close the hover card when clicking anywhere in the content | ||
| e.stopPropagation(); | ||
| }} | ||
| > | ||
| <div className="space-y-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <h3 className="font-semibold text-lg">Wallet Address</h3> | ||
| <Button | ||
| className="flex items-center gap-2" | ||
| onClick={onCopy} | ||
| size="sm" | ||
| variant="outline" | ||
| > | ||
| {hasCopied ? ( | ||
| <CheckIcon className="h-4 w-4" /> | ||
| ) : ( | ||
| <CopyIcon className="h-4 w-4" /> | ||
| )} | ||
| {hasCopied ? "Copied!" : "Copy"} | ||
| </Button> | ||
| <div className="space-y-1"> | ||
| <h3 className="font-medium text-sm">Wallet Address</h3> | ||
|
|
||
| <CopyTextButton | ||
| textToShow={address} | ||
| textToCopy={address} | ||
| tooltip="Copy address" | ||
| copyIconPosition="right" | ||
| variant="ghost" | ||
| className="text-muted-foreground -translate-x-1.5" | ||
| /> | ||
| </div> | ||
| <p className="rounded bg-muted p-2 text-center font-mono text-sm"> | ||
| {lessShortenedAddress} | ||
| </p> | ||
| <h3 className="font-semibold text-lg">Social Profiles</h3> | ||
| {profiles.isPending ? ( | ||
| <p className="text-muted-foreground text-sm">Loading profiles...</p> | ||
| ) : !profiles.data?.length ? ( | ||
| <p className="text-muted-foreground text-sm">No profiles found</p> | ||
| ) : ( | ||
| profiles.data?.map((profile) => { | ||
| const walletAvatarLink = resolveSchemeWithErrorHandler({ | ||
| client: props.client, | ||
| uri: profile.avatar, | ||
| }); | ||
|
|
||
| return ( | ||
| <div | ||
| className="flex flex-row items-center gap-2" | ||
| key={profile.type + profile.name} | ||
| > | ||
| {walletAvatarLink && ( | ||
| <Avatar> | ||
| <AvatarImage | ||
| alt={profile.name} | ||
| className="object-cover" | ||
| src={walletAvatarLink} | ||
| /> | ||
| {profile.name && ( | ||
| <AvatarFallback> | ||
| {profile.name.slice(0, 2)} | ||
| </AvatarFallback> | ||
| )} | ||
| </Avatar> | ||
| )} | ||
| <div className="flex w-full flex-col gap-1"> | ||
| <div className="flex w-full flex-row items-center justify-between gap-4"> | ||
| <h4 className="font-semibold text-md">{profile.name}</h4> | ||
| <Badge variant="outline">{profile.type}</Badge> | ||
|
|
||
| <div className="space-y-1 border-t pt-5 border-dashed"> | ||
| <h3 className="font-medium text-sm">Social Profiles</h3> | ||
|
|
||
| {props.profiles.isPending ? ( | ||
| <Skeleton className="h-4 w-[50%]" /> | ||
| ) : !props.profiles.data?.length ? ( | ||
| <p className="text-muted-foreground text-sm">No profiles found</p> | ||
| ) : ( | ||
| <div className="!mt-2"> | ||
| {props.profiles.data?.map((profile) => { | ||
| const walletAvatarLink = resolveSchemeWithErrorHandler({ | ||
| client: props.client, | ||
| uri: profile.avatar, | ||
| }); | ||
|
|
||
| return ( | ||
| <div | ||
| className="flex flex-row items-center gap-3 py-2" | ||
| key={profile.type + profile.name} | ||
| > | ||
| <Avatar className="size-9"> | ||
| <AvatarImage | ||
| alt={profile.name} | ||
| className="object-cover" | ||
| src={walletAvatarLink} | ||
| /> | ||
| {profile.name && ( | ||
| <AvatarFallback> | ||
| {profile.name.slice(0, 2)} | ||
| </AvatarFallback> | ||
| )} | ||
| </Avatar> | ||
|
|
||
| <div className="space-y-0.5"> | ||
| <h4 className="text-sm leading-none">{profile.name}</h4> | ||
| <span className="text-muted-foreground text-xs leading-none capitalize"> | ||
| {profile.type === "ens" ? "ENS" : profile.type} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| {profile.bio && ( | ||
| <p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm"> | ||
| {profile.bio} | ||
| </p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }) | ||
| )} | ||
| ); | ||
| })} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </HoverCardContent> | ||
| </HoverCard> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.