Skip to content

Commit d6527e3

Browse files
MananTankjnsdls
authored andcommitted
Add charts on usage page
1 parent 3b1df47 commit d6527e3

File tree

16 files changed

+593
-309
lines changed

16 files changed

+593
-309
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { UserOpStats } from "@/api/analytics";
2+
import { defineChain } from "thirdweb";
3+
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
4+
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
5+
6+
export async function TotalSponsoredChartCardUI({
7+
data,
8+
aggregatedData,
9+
searchParams,
10+
className,
11+
onlyMainnet,
12+
title,
13+
description,
14+
}: {
15+
data: UserOpStats[];
16+
aggregatedData: UserOpStats[];
17+
searchParams?: { [key: string]: string | string[] | undefined };
18+
className?: string;
19+
onlyMainnet?: boolean;
20+
title?: string;
21+
description?: string;
22+
}) {
23+
const chains = await Promise.all(
24+
data.map(
25+
(item) =>
26+
// eslint-disable-next-line no-restricted-syntax
27+
item.chainId && getChainMetadata(defineChain(Number(item.chainId))),
28+
),
29+
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
30+
31+
// Process data to combine by date and chain type
32+
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
33+
for (const item of data) {
34+
const chain = chains.find((c) => c.chainId === Number(item.chainId));
35+
36+
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
37+
if (chain?.testnet) {
38+
existing.testnet += item.sponsoredUsd;
39+
} else {
40+
existing.mainnet += item.sponsoredUsd;
41+
}
42+
dateMap.set(item.date, existing);
43+
}
44+
45+
// Convert to array and sort by date
46+
const timeSeriesData = Array.from(dateMap.entries())
47+
.map(([date, values]) => ({
48+
date,
49+
mainnet: values.mainnet,
50+
testnet: values.testnet,
51+
total: values.mainnet + values.testnet,
52+
}))
53+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
54+
55+
const processedAggregatedData = {
56+
mainnet: aggregatedData
57+
.filter(
58+
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
59+
)
60+
.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
61+
testnet: aggregatedData
62+
.filter(
63+
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
64+
)
65+
.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
66+
total: aggregatedData.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
67+
};
68+
69+
const chartConfig = {
70+
mainnet: {
71+
label: "Mainnet Chains",
72+
color: "hsl(var(--chart-1))",
73+
},
74+
testnet: {
75+
label: "Testnet Chains",
76+
color: "hsl(var(--chart-2))",
77+
},
78+
total: {
79+
label: "All Chains",
80+
color: "hsl(var(--chart-3))",
81+
},
82+
};
83+
84+
return (
85+
<CombinedBarChartCard
86+
isCurrency
87+
title={title || "Total Sponsored"}
88+
description={description}
89+
chartConfig={chartConfig}
90+
data={timeSeriesData}
91+
hideTabs={onlyMainnet}
92+
activeChart={
93+
onlyMainnet
94+
? "mainnet"
95+
: ((searchParams?.totalSponsored as keyof typeof chartConfig) ??
96+
"mainnet")
97+
}
98+
queryKey="totalSponsored"
99+
existingQueryParams={searchParams}
100+
aggregateFn={(_data, key) => processedAggregatedData[key]}
101+
className={className}
102+
// Get the trend from the last two COMPLETE periods
103+
trendFn={(data, key) =>
104+
data.filter((d) => (d[key] as number) > 0).length >= 3
105+
? ((data[data.length - 2]?.[key] as number) ?? 0) /
106+
((data[data.length - 3]?.[key] as number) ?? 0) -
107+
1
108+
: undefined
109+
}
110+
/>
111+
);
112+
}

apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx

Lines changed: 8 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { notFound, redirect } from "next/navigation";
88

99
import type {
1010
InAppWalletStats,
11-
UserOpStats,
1211
WalletStats,
1312
WalletUserStats,
1413
} from "@/api/analytics";
@@ -19,11 +18,6 @@ import {
1918
getLastNDaysRange,
2019
} from "components/analytics/date-range-selector";
2120

22-
import {
23-
type ChainMetadata,
24-
defineChain,
25-
getChainMetadata,
26-
} from "thirdweb/chains";
2721
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
2822
import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader";
2923
import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard";
@@ -34,6 +28,7 @@ import { getTeamBySlug } from "@/api/team";
3428
import { getAccount } from "app/account/settings/getAccount";
3529
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
3630
import { Changelog, type ChangelogItem } from "components/dashboard/Changelog";
31+
import { TotalSponsoredChartCardUI } from "./_components/TotalSponsoredCard";
3732

3833
// revalidate every 5 minutes
3934
export const revalidate = 300;
@@ -170,13 +165,12 @@ export default async function TeamOverviewPage(props: {
170165
)}
171166
</div>
172167
{userOpUsage.length > 0 ? (
173-
<div className="">
174-
<TotalSponsoredCard
175-
searchParams={searchParams}
176-
data={userOpUsageTimeSeries}
177-
aggregatedData={userOpUsage}
178-
/>
179-
</div>
168+
<TotalSponsoredChartCardUI
169+
searchParams={searchParams}
170+
data={userOpUsageTimeSeries}
171+
aggregatedData={userOpUsage}
172+
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
173+
/>
180174
) : (
181175
<EmptyStateCard
182176
metric="Sponsored Transactions"
@@ -257,6 +251,7 @@ function UsersChartCard({
257251

258252
return (
259253
<CombinedBarChartCard
254+
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
260255
title="Users"
261256
chartConfig={chartConfig}
262257
activeChart={
@@ -321,97 +316,3 @@ function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) {
321316
/>
322317
);
323318
}
324-
325-
async function TotalSponsoredCard({
326-
data,
327-
aggregatedData,
328-
searchParams,
329-
}: {
330-
data: UserOpStats[];
331-
aggregatedData: UserOpStats[];
332-
searchParams: { [key: string]: string | string[] | undefined };
333-
}) {
334-
const chains = await Promise.all(
335-
data.map(
336-
(item) =>
337-
// eslint-disable-next-line no-restricted-syntax
338-
item.chainId && getChainMetadata(defineChain(Number(item.chainId))),
339-
),
340-
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
341-
342-
// Process data to combine by date and chain type
343-
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
344-
for (const item of data) {
345-
const chain = chains.find((c) => c.chainId === Number(item.chainId));
346-
347-
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
348-
if (chain?.testnet) {
349-
existing.testnet += item.sponsoredUsd;
350-
} else {
351-
existing.mainnet += item.sponsoredUsd;
352-
}
353-
dateMap.set(item.date, existing);
354-
}
355-
356-
// Convert to array and sort by date
357-
const timeSeriesData = Array.from(dateMap.entries())
358-
.map(([date, values]) => ({
359-
date,
360-
mainnet: values.mainnet,
361-
testnet: values.testnet,
362-
total: values.mainnet + values.testnet,
363-
}))
364-
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
365-
366-
const processedAggregatedData = {
367-
mainnet: aggregatedData
368-
.filter(
369-
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
370-
)
371-
.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
372-
testnet: aggregatedData
373-
.filter(
374-
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
375-
)
376-
.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
377-
total: aggregatedData.reduce((acc, curr) => acc + curr.sponsoredUsd, 0),
378-
};
379-
380-
const chartConfig = {
381-
mainnet: {
382-
label: "Mainnet Chains",
383-
color: "hsl(var(--chart-1))",
384-
},
385-
testnet: {
386-
label: "Testnet Chains",
387-
color: "hsl(var(--chart-2))",
388-
},
389-
total: {
390-
label: "All Chains",
391-
color: "hsl(var(--chart-3))",
392-
},
393-
};
394-
395-
return (
396-
<CombinedBarChartCard
397-
isCurrency
398-
title="Total Sponsored"
399-
chartConfig={chartConfig}
400-
data={timeSeriesData}
401-
activeChart={
402-
(searchParams?.totalSponsored as keyof typeof chartConfig) ?? "mainnet"
403-
}
404-
queryKey="totalSponsored"
405-
existingQueryParams={searchParams}
406-
aggregateFn={(_data, key) => processedAggregatedData[key]}
407-
// Get the trend from the last two COMPLETE periods
408-
trendFn={(data, key) =>
409-
data.filter((d) => (d[key] as number) > 0).length >= 3
410-
? ((data[data.length - 2]?.[key] as number) ?? 0) /
411-
((data[data.length - 3]?.[key] as number) ?? 0) -
412-
1
413-
: undefined
414-
}
415-
/>
416-
);
417-
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ export function EcosystemWalletUsersChartCard(props: {
154154
chartData.every((data) => data[authMethod] === 0),
155155
) ? (
156156
<EmptyChartState>
157-
<div className="flex flex-col items-center justify-center">
158-
<span className="mb-6 text-lg">
157+
<div className="flex flex-col items-center justify-center px-4">
158+
<span className="mb-6 text-center text-lg">
159159
Connect users to your app with social logins
160160
</span>
161161
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">

apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,14 @@ export function PlanInfoCard(props: {
4646
</p>
4747
)}
4848
</div>
49-
<div>
49+
50+
<div className="flex flex-row gap-2">
51+
{/* manage team billing */}
5052
<BillingPortalButton teamSlug={team.slug} variant="outline">
5153
Manage Billing
5254
</BillingPortalButton>
53-
</div>
54-
55-
{isActualFreePlan && (
56-
<div>
57-
{/* manage team billing */}
58-
<BillingPortalButton teamSlug={team.slug} variant="outline">
59-
Manage Billing
60-
</BillingPortalButton>
6155

56+
{isActualFreePlan && (
6257
<Button asChild variant="outline">
6358
<TrackedLinkTW
6459
category="account"
@@ -70,8 +65,8 @@ export function PlanInfoCard(props: {
7065
View Pricing
7166
</TrackedLinkTW>
7267
</Button>
73-
</div>
74-
)}
68+
)}
69+
</div>
7570
</div>
7671

7772
<Separator />

apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/layout.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,30 @@ export default async function Layout(props: {
88
}) {
99
const params = await props.params;
1010
return (
11-
<SidebarLayout
12-
sidebarLinks={[
13-
{
14-
href: `/team/${params.team_slug}/~/usage`,
15-
exactMatch: true,
16-
label: "Overview",
17-
},
18-
{
19-
href: `/team/${params.team_slug}/~/usage/storage`,
20-
exactMatch: true,
21-
label: "Storage",
22-
},
23-
]}
24-
>
25-
{props.children}
26-
</SidebarLayout>
11+
<div className="flex grow flex-col">
12+
<div className="border-border border-b py-10">
13+
<div className="container">
14+
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
15+
Usage
16+
</h1>
17+
</div>
18+
</div>
19+
<SidebarLayout
20+
sidebarLinks={[
21+
{
22+
href: `/team/${params.team_slug}/~/usage`,
23+
exactMatch: true,
24+
label: "Overview",
25+
},
26+
{
27+
href: `/team/${params.team_slug}/~/usage/storage`,
28+
exactMatch: true,
29+
label: "Storage",
30+
},
31+
]}
32+
>
33+
{props.children}
34+
</SidebarLayout>
35+
</div>
2736
);
2837
}

0 commit comments

Comments
 (0)