Skip to content

Commit d4249c6

Browse files
committed
Merge branch 'main' into greg/fix-routes-page-crashing
2 parents b3758b5 + 70a519a commit d4249c6

File tree

45 files changed

+2936
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2936
-246
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ storybook-static
3131
.aider*
3232

3333
tsconfig.tsbuildinfo
34-
.cursor
34+
.cursor
35+
apps/dashboard/node-compile-cache
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
5+
export async function revalidatePathAction(
6+
path: string,
7+
type: "page" | "layout",
8+
) {
9+
revalidatePath(path, type);
10+
}

apps/dashboard/src/@/components/blocks/TokenSelector.stories.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,28 @@ function Variant(props: {
3535
label: string;
3636
selectedChainId?: number;
3737
}) {
38-
const [tokenAddress, setTokenAddress] = useState<string>("");
38+
const [token, setToken] = useState<
39+
| {
40+
address: string;
41+
chainId: number;
42+
}
43+
| undefined
44+
>(undefined);
45+
3946
return (
4047
<BadgeContainer label={props.label}>
4148
<TokenSelector
42-
tokenAddress={tokenAddress}
49+
addNativeTokenIfMissing={false}
50+
showCheck={false}
51+
selectedToken={token}
4352
chainId={props.selectedChainId}
4453
client={storybookThirdwebClient}
45-
onChange={setTokenAddress}
54+
onChange={(v) => {
55+
setToken({
56+
address: v.address,
57+
chainId: v.chainId,
58+
});
59+
}}
4660
/>
4761
</BadgeContainer>
4862
);

apps/dashboard/src/@/components/blocks/TokenSelector.tsx

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,103 @@
11
import { useCallback, useMemo } from "react";
2-
import type { ThirdwebClient } from "thirdweb";
2+
import {
3+
NATIVE_TOKEN_ADDRESS,
4+
type ThirdwebClient,
5+
getAddress,
6+
} from "thirdweb";
37
import { shortenAddress } from "thirdweb/utils";
8+
import { useAllChainsData } from "../../../hooks/chains/allChains";
49
import { useTokensData } from "../../../hooks/tokens/tokens";
510
import { replaceIpfsUrl } from "../../../lib/sdk";
611
import { fallbackChainIcon } from "../../../utils/chain-icons";
12+
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
713
import { cn } from "../../lib/utils";
814
import { Badge } from "../ui/badge";
915
import { Img } from "./Img";
1016
import { SelectWithSearch } from "./select-with-search";
1117

1218
type Option = { label: string; value: string };
1319

20+
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
21+
1422
export function TokenSelector(props: {
15-
tokenAddress: string | undefined;
16-
onChange: (tokenAddress: string) => void;
23+
selectedToken: { chainId: number; address: string } | undefined;
24+
onChange: (token: TokenMetadata) => void;
1725
className?: string;
1826
popoverContentClassName?: string;
1927
chainId?: number;
2028
side?: "left" | "right" | "top" | "bottom";
21-
disableChainId?: boolean;
29+
disableAddress?: boolean;
2230
align?: "center" | "start" | "end";
2331
placeholder?: string;
2432
client: ThirdwebClient;
2533
disabled?: boolean;
2634
enabled?: boolean;
35+
showCheck: boolean;
36+
addNativeTokenIfMissing: boolean;
2737
}) {
28-
const { tokens, isFetching } = useTokensData({
38+
const tokensQuery = useTokensData({
2939
chainId: props.chainId,
3040
enabled: props.enabled,
3141
});
3242

43+
const { idToChain } = useAllChainsData();
44+
45+
const tokens = useMemo(() => {
46+
if (!tokensQuery.data) {
47+
return [];
48+
}
49+
50+
if (props.addNativeTokenIfMissing) {
51+
const hasNativeToken = tokensQuery.data.some(
52+
(token) => token.address === checksummedNativeTokenAddress,
53+
);
54+
55+
if (!hasNativeToken && props.chainId) {
56+
return [
57+
{
58+
name:
59+
idToChain.get(props.chainId)?.nativeCurrency.name ??
60+
"Native Token",
61+
symbol:
62+
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
63+
decimals: 18,
64+
chainId: props.chainId,
65+
address: checksummedNativeTokenAddress,
66+
} satisfies TokenMetadata,
67+
...tokensQuery.data,
68+
];
69+
}
70+
}
71+
return tokensQuery.data;
72+
}, [
73+
tokensQuery.data,
74+
props.chainId,
75+
props.addNativeTokenIfMissing,
76+
idToChain,
77+
]);
78+
79+
const addressChainToToken = useMemo(() => {
80+
const value = new Map<string, TokenMetadata>();
81+
for (const token of tokens) {
82+
value.set(`${token.chainId}:${token.address}`, token);
83+
}
84+
return value;
85+
}, [tokens]);
86+
3387
const options = useMemo(() => {
34-
return tokens.allTokens.map((token) => {
35-
return {
36-
label: token.symbol,
37-
value: `${token.chainId}:${token.address}`,
38-
};
39-
});
40-
}, [tokens.allTokens]);
88+
return (
89+
tokens.map((token) => {
90+
return {
91+
label: token.symbol,
92+
value: `${token.chainId}:${token.address}`,
93+
};
94+
}) || []
95+
);
96+
}, [tokens]);
4197

4298
const searchFn = useCallback(
4399
(option: Option, searchValue: string) => {
44-
const token = tokens.addressChainToToken.get(option.value);
100+
const token = addressChainToToken.get(option.value);
45101
if (!token) {
46102
return false;
47103
}
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
55111
token.address.toLowerCase().includes(searchValue.toLowerCase())
56112
);
57113
},
58-
[tokens],
114+
[addressChainToToken],
59115
);
60116

61117
const renderOption = useCallback(
62118
(option: Option) => {
63-
const token = tokens.addressChainToToken.get(option.value);
119+
const token = addressChainToToken.get(option.value);
64120
if (!token) {
65121
return option.label;
66122
}
@@ -87,36 +143,46 @@ export function TokenSelector(props: {
87143
{token.symbol}
88144
</span>
89145

90-
{!props.disableChainId && (
91-
<Badge variant="outline" className="gap-2 max-sm:hidden">
146+
{!props.disableAddress && (
147+
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
92148
<span className="text-muted-foreground">Address</span>
93149
{shortenAddress(token.address, 4)}
94150
</Badge>
95151
)}
96152
</div>
97153
);
98154
},
99-
[tokens, props.disableChainId, props.client],
155+
[addressChainToToken, props.disableAddress, props.client],
100156
);
101157

158+
const selectedValue = props.selectedToken
159+
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
160+
: undefined;
161+
102162
return (
103163
<SelectWithSearch
104164
searchPlaceholder="Search by name or symbol"
105-
value={props.tokenAddress}
165+
value={selectedValue}
106166
options={options}
107167
onValueChange={(tokenAddress) => {
108-
props.onChange(tokenAddress);
168+
const token = addressChainToToken.get(tokenAddress);
169+
if (!token) {
170+
return;
171+
}
172+
props.onChange(token);
109173
}}
110174
closeOnSelect={true}
111-
showCheck={false}
175+
showCheck={props.showCheck}
112176
placeholder={
113-
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
177+
tokensQuery.isPending
178+
? "Loading Tokens..."
179+
: props.placeholder || "Select Token"
114180
}
115181
overrideSearchFn={searchFn}
116182
renderOption={renderOption}
117183
className={props.className}
118184
popoverContentClassName={props.popoverContentClassName}
119-
disabled={isFetching || props.disabled}
185+
disabled={tokensQuery.isPending || props.disabled}
120186
side={props.side}
121187
align={props.align}
122188
/>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { cn } from "@/lib/utils";
2+
3+
export type Segment = {
4+
label: string;
5+
percent: number;
6+
color: string;
7+
};
8+
9+
type DistributionBarChartProps = {
10+
segments: Segment[];
11+
title: string;
12+
};
13+
export function DistributionBarChart(props: DistributionBarChartProps) {
14+
const totalPercentage = props.segments.reduce(
15+
(sum, segment) => sum + segment.percent,
16+
0,
17+
);
18+
19+
const invalidTotalPercentage = totalPercentage !== 100;
20+
21+
return (
22+
<div>
23+
<div className="mb-2 flex items-center justify-between">
24+
<h3 className="font-medium text-sm">{props.title}</h3>
25+
<div
26+
className={cn(
27+
"font-medium text-muted-foreground text-sm",
28+
invalidTotalPercentage && "text-red-500",
29+
)}
30+
>
31+
Total: {totalPercentage}%
32+
</div>
33+
</div>
34+
35+
{/* Bar */}
36+
<div className="flex h-3 overflow-hidden rounded-lg">
37+
{props.segments.map((segment) => {
38+
return (
39+
<div
40+
key={segment.label}
41+
className="flex h-full items-center justify-center transition-all duration-200"
42+
style={{
43+
width: `${segment.percent}%`,
44+
backgroundColor: segment.color,
45+
}}
46+
/>
47+
);
48+
})}
49+
</div>
50+
51+
{/* Legends */}
52+
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
53+
{props.segments.map((segment) => {
54+
return (
55+
<div key={segment.label} className="flex items-center gap-1.5">
56+
<div
57+
className="size-3 rounded-full"
58+
style={{
59+
backgroundColor: segment.color,
60+
}}
61+
/>
62+
<p className="text-sm">
63+
{segment.label}: {segment.percent}%
64+
</p>
65+
</div>
66+
);
67+
})}
68+
</div>
69+
</div>
70+
);
71+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MultiStepStatus } from "./multi-step-status";
3+
4+
const meta = {
5+
title: "Blocks/MultiStepStatus",
6+
component: MultiStepStatus,
7+
decorators: [
8+
(Story) => (
9+
<div className="container w-full max-w-md py-10">
10+
<Story />
11+
</div>
12+
),
13+
],
14+
} satisfies Meta<typeof MultiStepStatus>;
15+
16+
export default meta;
17+
type Story = StoryObj<typeof meta>;
18+
19+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
20+
21+
export const AllStates: Story = {
22+
args: {
23+
steps: [
24+
{
25+
status: "completed",
26+
label: "Connect Wallet",
27+
retryLabel: "Failed to connect wallet",
28+
execute: async () => {
29+
await sleep(1000);
30+
},
31+
},
32+
{
33+
status: "pending",
34+
label: "Sign Message",
35+
retryLabel: "Failed to sign message",
36+
execute: async () => {
37+
await sleep(1000);
38+
},
39+
},
40+
{
41+
status: "error",
42+
label: "Approve Transaction",
43+
retryLabel: "Transaction approval failed",
44+
execute: async () => {
45+
await sleep(1000);
46+
},
47+
},
48+
{
49+
status: "idle",
50+
label: "Confirm Transaction",
51+
retryLabel: "Transaction confirmation failed",
52+
execute: async () => {
53+
await sleep(1000);
54+
},
55+
},
56+
{
57+
status: "idle",
58+
label: "Finalize",
59+
retryLabel: "Finalization failed",
60+
execute: async () => {
61+
await sleep(1000);
62+
},
63+
},
64+
],
65+
},
66+
};

0 commit comments

Comments
 (0)