Skip to content

Commit 28c2ea9

Browse files
committed
[NEB-213] Show wallet Assets in sidebar
1 parent 1191d5d commit 28c2ea9

File tree

5 files changed

+377
-24
lines changed

5 files changed

+377
-24
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { storybookThirdwebClient } from "../../../../../stories/utils";
3+
import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
4+
5+
const meta = {
6+
title: "Nebula/AssetsSection",
7+
component: AssetsSectionUI,
8+
decorators: [
9+
(Story) => (
10+
<div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
} satisfies Meta<typeof AssetsSectionUI>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
const tokensStub: AssetBalance[] = [
21+
{
22+
chain_id: 8453,
23+
token_address: "0xe8e55a847bb446d967ef92f4580162fb8f2d3f38",
24+
name: "Broge",
25+
symbol: "BROGE",
26+
decimals: 18,
27+
balance: "10000000000000000000000",
28+
},
29+
{
30+
chain_id: 8453,
31+
token_address: "0x8d2757ea27aabf172da4cca4e5474c76016e3dc5",
32+
name: "clBTC",
33+
symbol: "clBTC",
34+
decimals: 18,
35+
balance: "2",
36+
},
37+
{
38+
chain_id: 8453,
39+
token_address: "0xb56d0839998fd79efcd15c27cf966250aa58d6d3",
40+
name: "BASED USA",
41+
symbol: "USA",
42+
decimals: 18,
43+
balance: "1000000000000000000",
44+
},
45+
{
46+
chain_id: 8453,
47+
token_address: "0x600c9b69a65fb6d2551623a53ddef17b050233cd",
48+
name: "BearPaw",
49+
symbol: "PAW",
50+
decimals: 18,
51+
balance: "48888800000000000000",
52+
},
53+
{
54+
chain_id: 8453,
55+
token_address: "0x4c96a67b0577358894407af7bc3158fc1dffbeb5",
56+
name: "Degen Point Of View",
57+
symbol: "POV",
58+
decimals: 18,
59+
balance: "69000000000000000000",
60+
},
61+
{
62+
chain_id: 8453,
63+
token_address: "0x4200000000000000000000000000000000000006",
64+
name: "Wrapped Ether",
65+
symbol: "WETH",
66+
decimals: 18,
67+
balance: "6237535850425",
68+
},
69+
];
70+
71+
export const MultipleAssets: Story = {
72+
args: {
73+
data: tokensStub,
74+
isPending: false,
75+
client: storybookThirdwebClient,
76+
},
77+
};
78+
79+
export const SingleAsset: Story = {
80+
args: {
81+
data: tokensStub.slice(0, 1),
82+
isPending: false,
83+
client: storybookThirdwebClient,
84+
},
85+
};
86+
87+
export const EmptyAssets: Story = {
88+
args: {
89+
data: [],
90+
isPending: false,
91+
client: storybookThirdwebClient,
92+
},
93+
};
94+
95+
export const Loading: Story = {
96+
args: {
97+
data: [],
98+
isPending: true,
99+
client: storybookThirdwebClient,
100+
},
101+
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Skeleton } from "@/components/ui/skeleton";
2+
import { isProd } from "@/constants/env-utils";
3+
import { useQuery } from "@tanstack/react-query";
4+
import Link from "next/link";
5+
import { type ThirdwebClient, defineChain, toTokens } from "thirdweb";
6+
import {
7+
Blobbie,
8+
TokenIcon,
9+
TokenProvider,
10+
useActiveAccount,
11+
useActiveWalletChain,
12+
} from "thirdweb/react";
13+
import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
14+
import { useAllChainsData } from "../../../../../hooks/chains/allChains";
15+
import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
16+
17+
export type AssetBalance = {
18+
chain_id: number;
19+
token_address: string;
20+
balance: string;
21+
name: string;
22+
symbol: string;
23+
decimals: number;
24+
};
25+
26+
export function AssetsSectionUI(props: {
27+
data: AssetBalance[];
28+
isPending: boolean;
29+
client: ThirdwebClient;
30+
}) {
31+
return (
32+
<div className="flex flex-col gap-2">
33+
<h3 className="px-2 font-medium text-muted-foreground text-sm">Assets</h3>
34+
<div className="flex flex-col gap-1">
35+
{!props.isPending &&
36+
props.data.map((asset) => (
37+
<AssetItem
38+
key={asset.token_address}
39+
asset={asset}
40+
client={props.client}
41+
/>
42+
))}
43+
44+
{props.isPending &&
45+
new Array(10).fill(null).map((_, index) => (
46+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
47+
<SkeletonAssetItem key={index} />
48+
))}
49+
50+
{!props.isPending && props.data.length === 0 && (
51+
<div className="px-2 py-1 text-muted-foreground/70 text-sm">
52+
No assets found
53+
</div>
54+
)}
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
function SkeletonAssetItem() {
61+
return (
62+
<div className="flex h-[48px] items-center gap-2 px-2 py-1">
63+
<Skeleton className="size-8 rounded-full" />
64+
<div className="flex flex-col gap-1">
65+
<Skeleton className="h-3 w-32 bg-muted" />
66+
<Skeleton className="h-3 w-24 bg-muted" />
67+
</div>
68+
</div>
69+
);
70+
}
71+
72+
function AssetItem(props: {
73+
asset: AssetBalance;
74+
client: ThirdwebClient;
75+
}) {
76+
const { idToChain } = useAllChainsData();
77+
const chainMeta = idToChain.get(props.asset.chain_id);
78+
return (
79+
<TokenProvider
80+
address={props.asset.token_address}
81+
client={props.client}
82+
// eslint-disable-next-line no-restricted-syntax
83+
chain={defineChain(props.asset.chain_id)}
84+
>
85+
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
86+
<div className="relative">
87+
<TokenIcon
88+
className="size-8 rounded-full"
89+
loadingComponent={
90+
<Blobbie
91+
address={props.asset.token_address}
92+
className="size-8 rounded-full"
93+
/>
94+
}
95+
fallbackComponent={
96+
<Blobbie
97+
address={props.asset.token_address}
98+
className="size-8 rounded-full"
99+
/>
100+
}
101+
/>
102+
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
103+
<ChainIconClient
104+
client={props.client}
105+
className="size-3.5"
106+
src={chainMeta?.icon?.url || ""}
107+
/>
108+
</div>
109+
</div>
110+
111+
<div className="flex min-w-0 flex-col text-sm">
112+
<Link
113+
href={`https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`}
114+
target="_blank"
115+
className="truncate font-medium before:absolute before:inset-0"
116+
>
117+
{props.asset.name}
118+
</Link>
119+
120+
<p className="text-muted-foreground text-sm">
121+
{`${toTokens(BigInt(props.asset.balance), props.asset.decimals)} ${props.asset.symbol}`}
122+
</p>
123+
</div>
124+
</div>
125+
</TokenProvider>
126+
);
127+
}
128+
129+
export function AssetsSection(props: {
130+
client: ThirdwebClient;
131+
}) {
132+
const account = useActiveAccount();
133+
const activeChain = useActiveWalletChain();
134+
135+
const assetsQuery = useQuery({
136+
queryKey: ["assets", account?.address, activeChain?.id],
137+
queryFn: async () => {
138+
if (!account || !activeChain) {
139+
return [];
140+
}
141+
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
142+
const url = new URL(
143+
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
144+
);
145+
url.searchParams.set("limit", "50");
146+
url.searchParams.set("metadata", "true");
147+
url.searchParams.set("include_spam", "false");
148+
url.searchParams.set("clientId", chains.join(","));
149+
150+
for (const chain of chains) {
151+
url.searchParams.append("chain", chain.toString());
152+
}
153+
154+
url.searchParams.set("clientId", nebulaAppThirdwebClient.clientId);
155+
156+
const response = await fetch(url.toString());
157+
const json = (await response.json()) as {
158+
data: AssetBalance[];
159+
};
160+
161+
return json.data;
162+
},
163+
enabled: !!account && !!activeChain,
164+
});
165+
166+
return (
167+
<AssetsSectionUI
168+
data={assetsQuery.data ?? []}
169+
isPending={assetsQuery.isPending}
170+
client={props.client}
171+
/>
172+
);
173+
}

apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function ChatPageLayout(props: {
1717
props.className,
1818
)}
1919
>
20-
<aside className="hidden w-[280px] shrink-0 border-border border-r bg-card lg:block">
20+
<aside className="hidden w-[300px] shrink-0 border-border border-r bg-card lg:block">
2121
<ChatSidebar
2222
sessions={props.sessions}
2323
authToken={props.authToken}

0 commit comments

Comments
 (0)