Skip to content

Commit c40603f

Browse files
committed
[PRO-134] Add Bridge Volume pie chart in project overview page (#8488)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the analytics components by refining data formatting, adding currency support, and improving the UI for better data visualization in the dashboard. ### Detailed summary - Added `maximumFractionDigits` to formatting utilities. - Integrated currency formatting in `PieChart` and `TotalValueChartHeader`. - Updated `ProjectHighlightsCard` to remove total volume and adjust labels. - Introduced `BridgeChartCard` for displaying bridge usage data. - Enhanced `TotalVolumePieChart` to conditionally display total volume and adjusted data handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 458246f commit c40603f

File tree

10 files changed

+257
-137
lines changed

10 files changed

+257
-137
lines changed

apps/dashboard/src/@/components/analytics/empty-chart-state.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ export function EmptyChartState({
2828
type,
2929
}: {
3030
children?: React.ReactNode;
31-
type: "bar" | "area";
31+
type: "bar" | "area" | "none";
3232
}) {
3333
const barChartData = useMemo(() => generateRandomData(), []);
3434

3535
return (
3636
<div className="relative z-0 h-full w-full">
3737
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center text-base text-muted-foreground">
38-
{children ?? (
38+
{children || (
3939
<div className="flex items-center gap-3 flex-col">
4040
<div className="rounded-full border p-2 bg-background">
4141
<XIcon className="size-4" />
@@ -44,7 +44,7 @@ export function EmptyChartState({
4444
</div>
4545
)}
4646
</div>
47-
<SkeletonBarChart data={barChartData} type={type} />
47+
{type !== "none" && <SkeletonBarChart data={barChartData} type={type} />}
4848
</div>
4949
);
5050
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton";
66
export function TotalValueChartHeader(props: {
77
total: number;
88
title: string;
9+
isUsd?: boolean;
910
isPending: boolean;
1011
viewMoreLink: string | undefined;
1112
}) {
@@ -18,7 +19,9 @@ export function TotalValueChartHeader(props: {
1819
render={(value) => {
1920
return (
2021
<p className="text-3xl font-semibold tracking-tight">
21-
{compactNumberFormatter.format(value)}
22+
{props.isUsd
23+
? compactUSDFormatter.format(value)
24+
: compactNumberFormatter.format(value)}
2225
</p>
2326
);
2427
}}
@@ -48,3 +51,10 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
4851
notation: "compact",
4952
maximumFractionDigits: 2,
5053
});
54+
55+
const compactUSDFormatter = new Intl.NumberFormat("en-US", {
56+
notation: "compact",
57+
maximumFractionDigits: 2,
58+
style: "currency",
59+
currency: "USD",
60+
});

apps/dashboard/src/@/utils/format-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
22
notation: "compact",
3+
maximumFractionDigits: 2,
34
});
45

56
export const formatTickerNumber = (value: number) => {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ResponsiveSuspense } from "responsive-rsc";
2+
import { getUniversalBridgeUsage } from "@/api/analytics";
3+
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
4+
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
5+
import type { UniversalBridgeStats } from "@/types/analytics";
6+
import { TotalVolumePieChart } from "../payments/components/TotalVolumePieChart";
7+
8+
async function AsyncBridgeCard(props: {
9+
from: Date;
10+
to: Date;
11+
interval: "day" | "week";
12+
projectId: string;
13+
teamId: string;
14+
authToken: string;
15+
teamSlug: string;
16+
projectSlug: string;
17+
}) {
18+
const data = await getUniversalBridgeUsage(
19+
{
20+
from: props.from,
21+
period: props.interval,
22+
projectId: props.projectId,
23+
teamId: props.teamId,
24+
to: props.to,
25+
},
26+
props.authToken,
27+
).catch((error) => {
28+
console.error(error);
29+
return [];
30+
});
31+
32+
return (
33+
<BridgeChartCardUI
34+
data={data}
35+
teamSlug={props.teamSlug}
36+
projectSlug={props.projectSlug}
37+
/>
38+
);
39+
}
40+
41+
export function BridgeChartCard(props: {
42+
from: Date;
43+
to: Date;
44+
interval: "day" | "week";
45+
projectId: string;
46+
teamId: string;
47+
authToken: string;
48+
teamSlug: string;
49+
projectSlug: string;
50+
}) {
51+
return (
52+
<ResponsiveSuspense
53+
fallback={<LoadingChartState className="min-h-[40px] h-full border" />}
54+
searchParamsUsed={["from", "to", "interval"]}
55+
>
56+
<AsyncBridgeCard {...props} />
57+
</ResponsiveSuspense>
58+
);
59+
}
60+
61+
function BridgeChartCardUI(props: {
62+
data: UniversalBridgeStats[];
63+
teamSlug: string;
64+
projectSlug: string;
65+
}) {
66+
const total =
67+
props.data.reduce((acc, curr) => acc + curr.amountUsdCents, 0) / 100;
68+
69+
return (
70+
<div className="flex flex-col bg-card border rounded-lg">
71+
<TotalValueChartHeader
72+
title="Bridge Volume"
73+
total={total}
74+
isUsd={true}
75+
isPending={false}
76+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/bridge`}
77+
/>
78+
<div className="p-6 grow flex items-center justify-center">
79+
<TotalVolumePieChart data={props.data} hideTotal={true} />
80+
</div>
81+
</div>
82+
);
83+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/highlights-card-ui.tsx

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CombinedBarChartCard } from "../../../../components/Analytics/CombinedB
88
type AggregatedMetrics = {
99
activeUsers: number;
1010
newUsers: number;
11-
totalVolume: number;
1211
feesCollected: number;
1312
};
1413

@@ -37,18 +36,6 @@ export function ProjectHighlightsCard(props: {
3736
const chartConfig = {
3837
activeUsers: { color: "hsl(var(--chart-1))", label: "Active Users" },
3938
newUsers: { color: "hsl(var(--chart-3))", label: "New Users" },
40-
totalVolume: {
41-
color: "hsl(var(--chart-2))",
42-
emptyContent: (
43-
<EmptyStateContent
44-
description="Onramp, swap, and bridge with thirdweb's Payments."
45-
link="https://portal.thirdweb.com/payments"
46-
metric="Payments"
47-
/>
48-
),
49-
isCurrency: true,
50-
label: "Total Volume",
51-
},
5239
feesCollected: {
5340
color: "hsl(var(--chart-4))",
5441
emptyContent: (
@@ -59,7 +46,7 @@ export function ProjectHighlightsCard(props: {
5946
/>
6047
),
6148
isCurrency: true,
62-
label: "Bridge Fee Revenue",
49+
label: "Bridge Revenue",
6350
},
6451
} as const;
6552

@@ -129,14 +116,6 @@ function processTimeSeriesData(
129116
.filter((u) => new Date(u.date).toISOString().slice(0, 10) === date)
130117
.reduce((acc, curr) => acc + curr.newUsers, 0);
131118

132-
const volume = volumeStats
133-
.filter(
134-
(v) =>
135-
new Date(v.date).toISOString().slice(0, 10) === date &&
136-
v.status === "completed",
137-
)
138-
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
139-
140119
const fees = volumeStats
141120
.filter(
142121
(v) =>
@@ -150,7 +129,6 @@ function processTimeSeriesData(
150129
date: date,
151130
feesCollected: fees,
152131
newUsers: newUsers,
153-
totalVolume: volume,
154132
});
155133
}
156134

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/project-analytics.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LoadingChartState } from "@/components/analytics/empty-chart-state";
66
import { TransactionsChartCardAsync } from "../components/Transactions";
77
import { AIAnalyticsChartCard } from "./ai-card";
88
import { AllWalletConnectionsChart } from "./all-wallet-connections-chart";
9+
import { BridgeChartCard } from "./bridge-card";
910
import { ProjectHighlightCard } from "./highlights-card";
1011
import { IndexerRequestsChartCard } from "./indexer-card";
1112
import { RPCRequestsChartCard } from "./rpc-card";
@@ -37,17 +38,31 @@ export async function ProjectAnalytics(props: {
3738
searchParams={searchParams}
3839
/>
3940

40-
{/* wallets */}
41-
<AllWalletConnectionsChart
42-
teamSlug={params.team_slug}
43-
projectSlug={params.project_slug}
44-
authToken={authToken}
45-
projectId={project.id}
46-
from={range.from}
47-
to={range.to}
48-
interval={interval}
49-
teamId={project.teamId}
50-
/>
41+
<div className="grid gap-6 md:grid-cols-2">
42+
{/* wallets */}
43+
<AllWalletConnectionsChart
44+
teamSlug={params.team_slug}
45+
projectSlug={params.project_slug}
46+
authToken={authToken}
47+
projectId={project.id}
48+
from={range.from}
49+
to={range.to}
50+
interval={interval}
51+
teamId={project.teamId}
52+
/>
53+
54+
{/* bridge */}
55+
<BridgeChartCard
56+
teamSlug={params.team_slug}
57+
projectSlug={params.project_slug}
58+
from={range.from}
59+
to={range.to}
60+
interval={interval}
61+
projectId={project.id}
62+
teamId={project.teamId}
63+
authToken={authToken}
64+
/>
65+
</div>
5166

5267
<div className="grid gap-6 md:grid-cols-2">
5368
{/* Indexer */}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PayAnalytics.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,9 @@ export async function PayAnalytics(props: {
6767
walletDataPromise,
6868
]);
6969

70-
const hasVolume = volumeData.some((d) => d.amountUsdCents > 0);
71-
const hasWallet = walletData.some((d) => d.count > 0);
72-
73-
if (!hasVolume && !hasWallet) {
74-
return null;
75-
}
76-
7770
return (
7871
<div>
7972
<div>
80-
<div>
81-
<h2 className="text-xl font-semibold tracking-tight mb-1">
82-
Analytics
83-
</h2>
84-
<p className="text-muted-foreground mb-4 text-sm">
85-
Track Bridge volume, customers, payouts, and success rates.
86-
</p>
87-
</div>
8873
<div className="mb-4 flex justify-start">
8974
<PayAnalyticsFilter />
9075
</div>

0 commit comments

Comments
 (0)