Skip to content

Commit 4dcfb5b

Browse files
joaquim-vergesMananTank
authored andcommitted
[Dashboard] Fix asset page link and improve token claim UX (#7193)
1 parent 369a294 commit 4dcfb5b

File tree

17 files changed

+624
-174
lines changed

17 files changed

+624
-174
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Input } from "./input";
2+
export function DecimalInput(props: {
3+
value: string;
4+
onChange: (value: string) => void;
5+
maxValue?: number;
6+
id?: string;
7+
className?: string;
8+
}) {
9+
return (
10+
<Input
11+
id={props.id}
12+
type="text"
13+
value={props.value}
14+
className={props.className}
15+
inputMode="decimal"
16+
onChange={(e) => {
17+
const number = Number(e.target.value);
18+
// ignore if string becomes invalid number
19+
if (Number.isNaN(number)) {
20+
return;
21+
}
22+
if (props.maxValue && number > props.maxValue) {
23+
return;
24+
}
25+
// replace leading multiple zeros with single zero
26+
let cleanedValue = e.target.value.replace(/^0+/, "0");
27+
// replace leading zero before decimal point
28+
if (!cleanedValue.includes(".")) {
29+
cleanedValue = cleanedValue.replace(/^0+/, "");
30+
}
31+
props.onChange(cleanedValue || "0");
32+
}}
33+
/>
34+
);
35+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/ContractOverviewPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
5353
text: "View asset page",
5454
icon: <ExternalLinkIcon className="size-4" />,
5555
target: "_blank",
56-
link: `https://thirdweb.com/${chainSlug}/${contract.address}`,
56+
link: `/${chainSlug}/${contract.address}`,
5757
}}
5858
trackingCategory="erc20-contract"
5959
trackingLabel="view-asset-page"

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,22 @@ import {
77
useContractTransactionAnalytics,
88
useContractUniqueWalletAnalytics,
99
} from "data/analytics/hooks";
10+
import { differenceInCalendarDays, formatDate } from "date-fns";
1011
import { useTrack } from "hooks/analytics/useTrack";
1112
import { ArrowRightIcon } from "lucide-react";
1213
import Link from "next/link";
1314
import { useMemo, useState } from "react";
1415
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
1516
import { buildContractPagePath } from "../../_utils/contract-page-path";
1617

17-
function getDayKey(date: Date) {
18-
return date.toISOString().split("T")[0];
18+
function getDateKey(date: Date, precision: "day" | "hour") {
19+
const dayKey = date.toISOString().split("T")[0];
20+
if (precision === "day") {
21+
return dayKey;
22+
}
23+
24+
const hourKey = date.getHours();
25+
return `${dayKey}-${hourKey}`;
1926
}
2027

2128
export function ContractAnalyticsOverviewCard(props: {
@@ -59,33 +66,59 @@ export function ContractAnalyticsOverviewCard(props: {
5966
const isPending =
6067
wallets.isPending || transactions.isPending || events.isPending;
6168

62-
const mergedData = useMemo(() => {
69+
const { data, precision } = useMemo(() => {
6370
if (isPending) {
64-
return undefined;
71+
return {
72+
data: undefined,
73+
precision: "day" as const,
74+
};
6575
}
6676

6777
const time = (wallets.data || transactions.data || events.data || []).map(
6878
(wallet) => wallet.time,
6979
);
7080

71-
return time.map((time) => {
72-
const wallet = wallets.data?.find(
73-
(wallet) => getDayKey(wallet.time) === getDayKey(time),
74-
);
75-
const transaction = transactions.data?.find(
76-
(transaction) => getDayKey(transaction.time) === getDayKey(time),
77-
);
78-
const event = events.data?.find((event) => {
79-
return getDayKey(event.time) === getDayKey(time);
80-
});
81+
// if the time difference between the first and last time is less than 3 days - use hour precision
82+
const firstTime = time[0];
83+
const lastTime = time[time.length - 1];
84+
const timeDiff =
85+
firstTime && lastTime
86+
? differenceInCalendarDays(lastTime, firstTime)
87+
: undefined;
8188

82-
return {
83-
time,
84-
wallets: wallet?.count || 0,
85-
transactions: transaction?.count || 0,
86-
events: event?.count || 0,
87-
};
88-
});
89+
const precision: "day" | "hour" = !timeDiff
90+
? "hour"
91+
: timeDiff < 3
92+
? "hour"
93+
: "day";
94+
95+
return {
96+
data: time.map((time) => {
97+
const wallet = wallets.data?.find(
98+
(wallet) =>
99+
getDateKey(wallet.time, precision) === getDateKey(time, precision),
100+
);
101+
const transaction = transactions.data?.find(
102+
(transaction) =>
103+
getDateKey(transaction.time, precision) ===
104+
getDateKey(time, precision),
105+
);
106+
107+
const event = events.data?.find((event) => {
108+
return (
109+
getDateKey(event.time, precision) === getDateKey(time, precision)
110+
);
111+
});
112+
113+
return {
114+
time,
115+
wallets: wallet?.count || 0,
116+
transactions: transaction?.count || 0,
117+
events: event?.count || 0,
118+
};
119+
}),
120+
precision,
121+
};
89122
}, [wallets.data, transactions.data, events.data, isPending]);
90123

91124
const analyticsPath = buildContractPagePath({
@@ -111,10 +144,11 @@ export function ContractAnalyticsOverviewCard(props: {
111144
color: "hsl(var(--chart-3))",
112145
},
113146
}}
114-
data={mergedData || []}
147+
data={data || []}
115148
isPending={isPending}
116149
showLegend
117150
chartClassName="aspect-[1.5] lg:aspect-[3]"
151+
toolTipLabelFormatter={toolTipLabelFormatterWithPrecision(precision)}
118152
customHeader={
119153
<div className="flex items-center justify-between gap-4 border-b p-6 py-4">
120154
<h2 className="font-semibold text-xl tracking-tight">Analytics</h2>
@@ -141,3 +175,16 @@ export function ContractAnalyticsOverviewCard(props: {
141175
/>
142176
);
143177
}
178+
179+
function toolTipLabelFormatterWithPrecision(precision: "day" | "hour") {
180+
return function toolTipLabelFormatter(_v: string, item: unknown) {
181+
if (Array.isArray(item)) {
182+
const time = item[0].payload.time as number;
183+
return formatDate(
184+
new Date(time),
185+
precision === "day" ? "MMM d, yyyy" : "MMM d, yyyy hh:mm a",
186+
);
187+
}
188+
return undefined;
189+
};
190+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { storybookThirdwebClient } from "stories/utils";
3+
import { getContract } from "thirdweb";
4+
import type { ChainMetadata } from "thirdweb/chains";
5+
import { ThirdwebProvider } from "thirdweb/react";
6+
import { ContractHeaderUI } from "./ContractHeader";
7+
8+
const meta = {
9+
title: "ERC20/ContractHeader",
10+
component: ContractHeaderUI,
11+
parameters: {
12+
nextjs: {
13+
appDirectory: true,
14+
},
15+
},
16+
decorators: [
17+
(Story) => (
18+
<ThirdwebProvider>
19+
<div className="container max-w-7xl py-10">
20+
<Story />
21+
</div>
22+
</ThirdwebProvider>
23+
),
24+
],
25+
} satisfies Meta<typeof ContractHeaderUI>;
26+
27+
export default meta;
28+
type Story = StoryObj<typeof meta>;
29+
30+
const mockTokenImage =
31+
"ipfs://ipfs/QmXYgTEavjF6c9X1a2pt5E379MYqSwFzzKvsUbSnRiSUEc/ea207d218948137.67aa26cfbd956.png";
32+
33+
const ethereumChainMetadata: ChainMetadata = {
34+
name: "Ethereum Mainnet",
35+
chain: "ethereum",
36+
chainId: 1,
37+
networkId: 1,
38+
nativeCurrency: {
39+
name: "Ether",
40+
symbol: "ETH",
41+
decimals: 18,
42+
},
43+
rpc: ["https://eth.llamarpc.com"],
44+
shortName: "eth",
45+
slug: "ethereum",
46+
testnet: false,
47+
icon: {
48+
url: "https://thirdweb.com/chain-icons/ethereum.svg",
49+
width: 24,
50+
height: 24,
51+
format: "svg",
52+
},
53+
explorers: [
54+
{
55+
name: "Etherscan",
56+
url: "https://etherscan.io",
57+
standard: "EIP3091",
58+
},
59+
],
60+
stackType: "evm",
61+
};
62+
63+
const mockContract = getContract({
64+
client: storybookThirdwebClient,
65+
chain: {
66+
id: 1,
67+
name: "Ethereum",
68+
rpc: "https://eth.llamarpc.com",
69+
},
70+
address: "0x1234567890123456789012345678901234567890",
71+
});
72+
73+
const mockSocialUrls = {
74+
twitter: "https://twitter.com",
75+
discord: "https://discord.gg",
76+
telegram: "https://web.telegram.org/",
77+
website: "https://example.com",
78+
github: "https://github.com",
79+
linkedin: "https://linkedin.com",
80+
tiktok: "https://tiktok.com",
81+
instagram: "https://instagram.com",
82+
custom: "https://example.com",
83+
reddit: "https://reddit.com",
84+
youtube: "https://youtube.com",
85+
};
86+
87+
export const WithImageAndMultipleSocialUrls: Story = {
88+
args: {
89+
name: "Sample Token",
90+
symbol: "SMPL",
91+
image: mockTokenImage,
92+
chainMetadata: ethereumChainMetadata,
93+
clientContract: mockContract,
94+
socialUrls: {
95+
twitter: mockSocialUrls.twitter,
96+
discord: mockSocialUrls.discord,
97+
telegram: mockSocialUrls.telegram,
98+
website: mockSocialUrls.website,
99+
github: mockSocialUrls.github,
100+
},
101+
},
102+
};
103+
104+
export const WithBrokenImageAndSingleSocialUrl: Story = {
105+
args: {
106+
name: "Sample Token",
107+
symbol: "SMPL",
108+
image: "broken-image.png",
109+
chainMetadata: ethereumChainMetadata,
110+
clientContract: mockContract,
111+
socialUrls: {
112+
website: mockSocialUrls.website,
113+
},
114+
},
115+
};
116+
117+
export const WithoutImageAndNoSocialUrls: Story = {
118+
args: {
119+
name: "Sample Token",
120+
symbol: "SMPL",
121+
image: undefined,
122+
chainMetadata: ethereumChainMetadata,
123+
clientContract: mockContract,
124+
socialUrls: {},
125+
},
126+
};
127+
128+
export const LongNameAndLotsOfSocialUrls: Story = {
129+
args: {
130+
name: "This is a very long token name that should wrap to multiple lines",
131+
symbol: "LONG",
132+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
133+
chainMetadata: ethereumChainMetadata,
134+
clientContract: mockContract,
135+
socialUrls: {
136+
twitter: mockSocialUrls.twitter,
137+
discord: mockSocialUrls.discord,
138+
telegram: mockSocialUrls.telegram,
139+
reddit: mockSocialUrls.reddit,
140+
youtube: mockSocialUrls.youtube,
141+
website: mockSocialUrls.website,
142+
github: mockSocialUrls.github,
143+
},
144+
},
145+
};
146+
147+
export const AllSocialUrls: Story = {
148+
args: {
149+
name: "Sample Token",
150+
symbol: "SMPL",
151+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
152+
chainMetadata: ethereumChainMetadata,
153+
clientContract: mockContract,
154+
socialUrls: {
155+
twitter: mockSocialUrls.twitter,
156+
discord: mockSocialUrls.discord,
157+
telegram: mockSocialUrls.telegram,
158+
reddit: mockSocialUrls.reddit,
159+
youtube: mockSocialUrls.youtube,
160+
website: mockSocialUrls.website,
161+
github: mockSocialUrls.github,
162+
linkedin: mockSocialUrls.linkedin,
163+
tiktok: mockSocialUrls.tiktok,
164+
instagram: mockSocialUrls.instagram,
165+
custom: mockSocialUrls.custom,
166+
},
167+
},
168+
};
169+
170+
export const InvalidSocialUrls: Story = {
171+
args: {
172+
name: "Sample Token",
173+
symbol: "SMPL",
174+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
175+
chainMetadata: ethereumChainMetadata,
176+
clientContract: mockContract,
177+
socialUrls: {
178+
twitter: "invalid-url",
179+
discord: "invalid-url",
180+
telegram: "invalid-url",
181+
reddit: "",
182+
youtube: mockSocialUrls.youtube,
183+
},
184+
},
185+
};
186+
187+
export const SomeSocialUrls: Story = {
188+
args: {
189+
name: "Sample Token",
190+
symbol: "SMPL",
191+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
192+
chainMetadata: ethereumChainMetadata,
193+
clientContract: mockContract,
194+
socialUrls: {
195+
website: mockSocialUrls.website,
196+
twitter: mockSocialUrls.twitter,
197+
},
198+
},
199+
};

0 commit comments

Comments
 (0)