Skip to content

Commit 1e092a7

Browse files
committed
Dashboard: Fix timestamp ordering in various charts, migrate them to use ThirdwebBarChart (#6636)
<!-- start pr-codex --> ## PR-Codex overview This PR primarily focuses on updating various components to improve chart functionalities and data handling in the dashboard, including the introduction of the `ThirdwebBarChart`. It also refines date handling and enhances the user interface for better data representation. ### Detailed summary - Updated date handling in `stubbedData` from February to December. - Introduced `BadgeContainer` for displaying time ranges in `TotalSponsoredChartCard`. - Enhanced `searchParams` structure in the analytics page to include `from` and `to` dates. - Added `customHeader`, `toolTipValueFormatter`, and `emptyChartState` to `ThirdwebBarChart`. - Replaced `BarChart` with `ThirdwebBarChart` in multiple components for consistency. - Improved data mapping and sorting in `TotalSponsoredChartCard`. - Created new stubs for `EcosystemWalletUsersChartCard` to simulate user data over various days. - Refactored empty state components to improve user guidance when no data is available. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 80ee3d6 commit 1e092a7

File tree

9 files changed

+477
-527
lines changed

9 files changed

+477
-527
lines changed

apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
3232
description?: string;
3333
titleClassName?: string;
3434
};
35+
customHeader?: React.ReactNode;
3536
// chart config
3637
config: TConfig;
3738
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
4142
chartClassName?: string;
4243
isPending: boolean;
4344
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
45+
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
4446
hideLabel?: boolean;
47+
emptyChartState?: React.ReactElement;
4548
};
4649

4750
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
6568
</CardHeader>
6669
)}
6770

71+
{props.customHeader && props.customHeader}
72+
6873
<CardContent className={cn(!props.header && "pt-6")}>
6974
<ChartContainer config={props.config} className={props.chartClassName}>
7075
{props.isPending ? (
7176
<LoadingChartState />
7277
) : props.data.length === 0 ? (
73-
<EmptyChartState />
78+
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
7479
) : (
7580
<BarChart accessibilityLayer data={props.data}>
7681
<CartesianGrid vertical={false} />
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
8893
props.hideLabel !== undefined ? props.hideLabel : true
8994
}
9095
labelFormatter={props.toolTipLabelFormatter}
96+
valueFormatter={props.toolTipValueFormatter}
9197
/>
9298
}
9399
/>
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
96102
content={<ChartLegendContent className="pt-5" />}
97103
/>
98104
)}
99-
{configKeys.map((key, idx) => (
105+
{configKeys.map((key) => (
100106
<Bar
101107
key={key}
102108
dataKey={key}
103109
// if stacked then they should all be the same stackId
104110
// if grouped then they should all be unique stackId (so the key works great)
105111
stackId={variant === "stacked" ? "a" : key}
106-
fill={`var(--color-${key})`}
107-
// if stacked then we need to figure out the radius based on the index in the array
108-
// if grouped then we can just use the same radius for all
109-
radius={
110-
variant === "stacked"
111-
? idx === 0
112-
? [0, 0, 4, 4]
113-
: idx === configKeys.length - 1
114-
? [4, 4, 0, 0]
115-
: [0, 0, 0, 0]
116-
: [4, 4, 4, 4]
117-
}
112+
fill={props.config[key]?.color}
113+
radius={[4, 4, 4, 4]}
118114
strokeWidth={1}
119115
className="stroke-background"
120116
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import type { EcosystemWalletStats } from "types/analytics";
3+
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";
4+
5+
const meta = {
6+
title: "Ecosystem/Analytics/EcosystemWalletUsersChartCard",
7+
component: EcosystemWalletUsersChartCard,
8+
decorators: [
9+
(Story) => (
10+
<div className="container max-w-7xl py-10">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
} satisfies Meta<typeof EcosystemWalletUsersChartCard>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
const authMethods = [
21+
"Email",
22+
"Google",
23+
"Apple",
24+
"Discord",
25+
"Twitter",
26+
"GitHub",
27+
"Facebook",
28+
"Twitch",
29+
"LinkedIn",
30+
"TikTok",
31+
"Coinbase",
32+
"MetaMask",
33+
];
34+
35+
function ecosystemWalletStatsStub(
36+
length: number,
37+
startDate = new Date(2024, 11, 1),
38+
): EcosystemWalletStats[] {
39+
const stats: EcosystemWalletStats[] = [];
40+
41+
for (let i = 0; i < length; i++) {
42+
const date = new Date(startDate);
43+
date.setDate(date.getDate() + i);
44+
const formattedDate = date.toISOString().split("T")[0] || "";
45+
46+
// each day, we pick between 1 and 4 auth methods
47+
const authMethodsToPick = Math.floor(Math.random() * 4) + 1;
48+
49+
for (let j = 0; j < authMethodsToPick; j++) {
50+
const authMethod =
51+
authMethods[Math.floor(Math.random() * authMethods.length)];
52+
stats.push({
53+
date: formattedDate,
54+
authenticationMethod: authMethod || "MetaMask",
55+
uniqueWalletsConnected: Math.floor(Math.random() * 1000) + 1,
56+
});
57+
}
58+
}
59+
60+
return stats;
61+
}
62+
63+
// Empty data state
64+
export const EmptyData: Story = {
65+
args: {
66+
ecosystemWalletStats: [],
67+
isPending: false,
68+
},
69+
};
70+
71+
// Loading state
72+
export const Loading: Story = {
73+
args: {
74+
ecosystemWalletStats: [],
75+
isPending: true,
76+
},
77+
};
78+
79+
// 30 days of data
80+
export const ThirtyDaysData: Story = {
81+
args: {
82+
ecosystemWalletStats: ecosystemWalletStatsStub(30),
83+
isPending: false,
84+
},
85+
};
86+
87+
// 60 days of data
88+
export const SixtyDaysData: Story = {
89+
args: {
90+
ecosystemWalletStats: ecosystemWalletStatsStub(60),
91+
isPending: false,
92+
},
93+
};
94+
95+
// 120 days of data
96+
export const OneHundredTwentyDaysData: Story = {
97+
args: {
98+
ecosystemWalletStats: ecosystemWalletStatsStub(120),
99+
isPending: false,
100+
},
101+
};
102+
103+
// Data with lots of authentication methods to test the "Others" category
104+
export const ManyAuthMethods: Story = {
105+
args: {
106+
ecosystemWalletStats: (() => {
107+
// Generate data with 15 different auth methods to test "Others" category
108+
const basicData = ecosystemWalletStatsStub(30);
109+
110+
return basicData.map((item, index) => ({
111+
...item,
112+
authenticationMethod:
113+
authMethods[index % authMethods.length] || "MetaMask",
114+
}));
115+
})(),
116+
isPending: false,
117+
},
118+
};
119+
120+
// Zero values test
121+
export const ZeroValues: Story = {
122+
args: {
123+
ecosystemWalletStats: ecosystemWalletStatsStub(30).map((stat) => ({
124+
...stat,
125+
uniqueWalletsConnected: 0,
126+
})),
127+
isPending: false,
128+
},
129+
};

0 commit comments

Comments
 (0)