Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions apps/dashboard/src/@/components/blocks/wallet-address.stories.tsx
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",
],
},
},
];
172 changes: 92 additions & 80 deletions apps/dashboard/src/@/components/blocks/wallet-address.tsx
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;
}) {
};

export function WalletAddress(props: WalletAddressProps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit return types to exported functions.

Both WalletAddress and WalletAddressUI are missing explicit return types. Coding guidelines require explicit return types for all function declarations in TypeScript.

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
In apps/dashboard/src/@/components/blocks/wallet-address.tsx around lines 29 and
46, the exported functions WalletAddress and WalletAddressUI lack explicit
return types; add explicit TypeScript return annotations (e.g., : JSX.Element or
: React.ReactElement) to both function declarations so they conform to the
coding guideline for exported functions, ensuring imports for React types exist
if needed.

const profiles = useSocialProfiles({
address: props.address,
client: props.client,
});

return (
<WalletAddressUI
{...props}
profiles={{
data: profiles.data || [],
isPending: profiles.isPending,
}}
/>
);
}

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)}`
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
return await getSocialProfiles({ address, client });
},
queryKey: ["social-profiles", address],
retry: 3,
retry: false,

Check warning on line 40 in packages/thirdweb/src/react/core/social/useSocialProfiles.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/social/useSocialProfiles.ts#L40

Added line #L40 was not covered by tests
});
}
Loading