Skip to content

Commit c382517

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

File tree

17 files changed

+610
-174
lines changed

17 files changed

+610
-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,185 @@
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+
export const WithImageAndMultipleSocialUrls: Story = {
74+
args: {
75+
name: "Sample Token",
76+
symbol: "SMPL",
77+
image: mockTokenImage,
78+
chainMetadata: ethereumChainMetadata,
79+
clientContract: mockContract,
80+
socialUrls: {
81+
twitter: "https://twitter.com/sample",
82+
discord: "https://discord.gg/sample",
83+
telegram: "https://t.me/sample",
84+
website: "https://sample.com",
85+
github: "https://github.com/sample",
86+
},
87+
},
88+
};
89+
90+
export const WithBrokenImageAndSingleSocialUrl: Story = {
91+
args: {
92+
name: "Sample Token",
93+
symbol: "SMPL",
94+
image: "broken-image.png",
95+
chainMetadata: ethereumChainMetadata,
96+
clientContract: mockContract,
97+
socialUrls: {
98+
website: "https://sample.com",
99+
},
100+
},
101+
};
102+
103+
export const WithoutImageAndNoSocialUrls: Story = {
104+
args: {
105+
name: "Sample Token",
106+
symbol: "SMPL",
107+
image: undefined,
108+
chainMetadata: ethereumChainMetadata,
109+
clientContract: mockContract,
110+
socialUrls: {},
111+
},
112+
};
113+
114+
export const LongNameAndLotsOfSocialUrls: Story = {
115+
args: {
116+
name: "This is a very long token name that should wrap to multiple lines",
117+
symbol: "LONG",
118+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
119+
chainMetadata: ethereumChainMetadata,
120+
clientContract: mockContract,
121+
socialUrls: {
122+
twitter: "https://twitter.com/sample",
123+
discord: "https://discord.gg/sample",
124+
telegram: "https://t.me/sample",
125+
reddit: "https://reddit.com/r/sample",
126+
youtube: "https://youtube.com/@sample",
127+
website: "https://sample.com",
128+
github: "https://github.com/sample",
129+
},
130+
},
131+
};
132+
133+
export const AllSocialUrls: Story = {
134+
args: {
135+
name: "Sample Token",
136+
symbol: "SMPL",
137+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
138+
chainMetadata: ethereumChainMetadata,
139+
clientContract: mockContract,
140+
socialUrls: {
141+
twitter: "https://twitter.com/sample",
142+
discord: "https://discord.gg/sample",
143+
telegram: "https://t.me/sample",
144+
reddit: "https://reddit.com/r/sample",
145+
youtube: "https://youtube.com/@sample",
146+
website: "https://sample.com",
147+
github: "https://github.com/sample",
148+
linkedin: "https://linkedin.com/in/sample",
149+
tiktok: "https://tiktok.com/@sample",
150+
instagram: "https://instagram.com/sample",
151+
custom: "https://custom.com",
152+
},
153+
},
154+
};
155+
156+
export const InvalidSocialUrls: Story = {
157+
args: {
158+
name: "Sample Token",
159+
symbol: "SMPL",
160+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
161+
chainMetadata: ethereumChainMetadata,
162+
clientContract: mockContract,
163+
socialUrls: {
164+
twitter: "invalid-url",
165+
discord: "invalid-url",
166+
telegram: "invalid-url",
167+
reddit: "",
168+
youtube: "https://youtube.com/@sample",
169+
},
170+
},
171+
};
172+
173+
export const SomeSocialUrls: Story = {
174+
args: {
175+
name: "Sample Token",
176+
symbol: "SMPL",
177+
image: "https://thirdweb.com/chain-icons/ethereum.svg",
178+
chainMetadata: ethereumChainMetadata,
179+
clientContract: mockContract,
180+
socialUrls: {
181+
website: "https://sample.com",
182+
twitter: "https://twitter.com/sample",
183+
},
184+
},
185+
};

0 commit comments

Comments
 (0)