Skip to content

Commit 5503801

Browse files
committed
CORE-[656] Update Dashboard Team layout
1 parent 07ad025 commit 5503801

File tree

20 files changed

+380
-378
lines changed

20 files changed

+380
-378
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export default async function TeamLayout(props: {
5454
exactMatch: true,
5555
},
5656
{
57-
path: `/team/${params.team_slug}/~/projects`,
58-
name: "Projects",
57+
path: `/team/${params.team_slug}/~/analytics`,
58+
name: "Analytics",
5959
},
6060
{
6161
path: `/team/${params.team_slug}/~/contracts`,
Lines changed: 16 additions & 319 deletions
Original file line numberDiff line numberDiff line change
@@ -1,336 +1,33 @@
1-
import {
2-
getInAppWalletUsage,
3-
getUserOpUsage,
4-
getWalletConnections,
5-
getWalletUsers,
6-
} from "@/api/analytics";
7-
import { redirect } from "next/navigation";
8-
9-
import type {
10-
InAppWalletStats,
11-
WalletStats,
12-
WalletUserStats,
13-
} from "types/analytics";
14-
15-
import {
16-
type DurationId,
17-
type Range,
18-
getLastNDaysRange,
19-
} from "components/analytics/date-range-selector";
20-
21-
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
22-
import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader";
23-
import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard";
24-
import { EmptyState } from "../../components/Analytics/EmptyState";
25-
import { PieChartCard } from "../../components/Analytics/PieChartCard";
26-
1+
import { getProjects } from "@/api/projects";
272
import { getTeamBySlug } from "@/api/team";
28-
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
29-
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
30-
import { getValidAccount } from "app/account/settings/getAccount";
31-
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
32-
import { Changelog, type ChangelogItem } from "components/dashboard/Changelog";
33-
import { Suspense } from "react";
34-
import { TotalSponsoredChartCardUI } from "./_components/TotalSponsoredCard";
35-
36-
// revalidate every 5 minutes
37-
export const revalidate = 300;
38-
39-
type SearchParams = {
40-
usersChart?: string;
41-
from?: string;
42-
to?: string;
43-
type?: string;
44-
interval?: string;
45-
};
3+
import { Changelog } from "components/dashboard/Changelog";
4+
import { redirect } from "next/navigation";
5+
import { TeamProjectsPage } from "./~/projects/TeamProjectsPage";
466

47-
export default async function TeamOverviewPage(props: {
7+
export default async function Page(props: {
488
params: Promise<{ team_slug: string }>;
49-
searchParams: Promise<SearchParams>;
509
}) {
51-
const changelog = await getChangelog();
52-
const [params, searchParams] = await Promise.all([
53-
props.params,
54-
props.searchParams,
55-
]);
56-
57-
const account = await getValidAccount(`/team/${params.team_slug}`);
10+
const params = await props.params;
5811
const team = await getTeamBySlug(params.team_slug);
5912

6013
if (!team) {
6114
redirect("/team");
6215
}
6316

64-
const interval = (searchParams.interval as "day" | "week") ?? "week";
65-
const rangeType = (searchParams.type as DurationId) || "last-120";
66-
const range: Range = {
67-
from: new Date(searchParams.from ?? getLastNDaysRange("last-120").from),
68-
to: new Date(searchParams.to ?? getLastNDaysRange("last-120").to),
69-
type: rangeType,
70-
};
17+
const projects = await getProjects(params.team_slug);
7118

7219
return (
73-
<div className="flex grow flex-col">
74-
<div className="border-b">
75-
<AnalyticsHeader
76-
title="Team Overview"
77-
interval={interval}
78-
range={range}
79-
/>
20+
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
21+
<div className="flex grow flex-col">
22+
<h1 className="mb-4 font-semibold text-xl tracking-tight">Projects</h1>
23+
<TeamProjectsPage projects={projects} team={team} />
8024
</div>
81-
<div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16 xl:flex-row">
82-
<Suspense fallback={<GenericLoadingPage />}>
83-
<OverviewPageContent
84-
account={account}
85-
range={range}
86-
interval={interval}
87-
searchParams={searchParams}
88-
/>
89-
</Suspense>
90-
<div className="shrink-0 max-md:container max-xl:hidden lg:w-[320px]">
91-
<h2 className="mb-4 font-semibold text-lg tracking-tight">
92-
Latest changes
93-
</h2>
94-
<Changelog changelog={changelog} />
95-
</div>
96-
</div>
97-
</div>
98-
);
99-
}
100-
101-
async function OverviewPageContent(props: {
102-
account: Account;
103-
range: Range;
104-
interval: "day" | "week";
105-
searchParams: SearchParams;
106-
}) {
107-
const { account, range, interval, searchParams } = props;
108-
109-
const [
110-
walletConnections,
111-
walletUserStatsTimeSeries,
112-
inAppWalletUsage,
113-
userOpUsageTimeSeries,
114-
userOpUsage,
115-
] = await Promise.all([
116-
// Aggregated wallet connections
117-
getWalletConnections({
118-
accountId: account.id,
119-
from: range.from,
120-
to: range.to,
121-
period: "all",
122-
}),
123-
// Time series data for wallet users
124-
getWalletUsers({
125-
accountId: account.id,
126-
from: range.from,
127-
to: range.to,
128-
period: interval,
129-
}),
130-
// In-app wallet usage
131-
getInAppWalletUsage({
132-
accountId: account.id,
133-
from: range.from,
134-
to: range.to,
135-
period: "all",
136-
}),
137-
// User operations usage
138-
getUserOpUsage({
139-
accountId: account.id,
140-
from: range.from,
141-
to: range.to,
142-
period: interval,
143-
}),
144-
getUserOpUsage({
145-
accountId: account.id,
146-
from: range.from,
147-
to: range.to,
148-
period: "all",
149-
}),
150-
]);
151-
152-
const isEmpty =
153-
!walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) &&
154-
walletConnections.length === 0 &&
155-
inAppWalletUsage.length === 0 &&
156-
userOpUsage.length === 0;
157-
158-
if (isEmpty) {
159-
return <EmptyState />;
160-
}
161-
162-
return (
163-
<div className="flex grow flex-col gap-6">
164-
{walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? (
165-
<div className="">
166-
<UsersChartCard
167-
userStats={walletUserStatsTimeSeries}
168-
searchParams={searchParams}
169-
/>
170-
</div>
171-
) : (
172-
<EmptyStateCard
173-
metric="Connect"
174-
link="https://portal.thirdweb.com/connect/quickstart"
175-
/>
176-
)}
177-
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
178-
{walletConnections.length > 0 ? (
179-
<WalletDistributionCard data={walletConnections} />
180-
) : (
181-
<EmptyStateCard
182-
metric="Connect"
183-
link="https://portal.thirdweb.com/connect/quickstart"
184-
/>
185-
)}
186-
{inAppWalletUsage.length > 0 ? (
187-
<AuthMethodDistributionCard data={inAppWalletUsage} />
188-
) : (
189-
<EmptyStateCard
190-
metric="In-App Wallets"
191-
link="https://portal.thirdweb.com/typescript/v5/inAppWallet"
192-
/>
193-
)}
25+
<div className="shrink-0 lg:w-[320px]">
26+
<h2 className="mb-4 font-semibold text-xl tracking-tight">
27+
Latest changes
28+
</h2>
29+
<Changelog />
19430
</div>
195-
{userOpUsage.length > 0 ? (
196-
<TotalSponsoredChartCardUI
197-
searchParams={searchParams}
198-
data={userOpUsageTimeSeries}
199-
aggregatedData={userOpUsage}
200-
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
201-
/>
202-
) : (
203-
<EmptyStateCard
204-
metric="Sponsored Transactions"
205-
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
206-
/>
207-
)}
20831
</div>
20932
);
21033
}
211-
212-
async function getChangelog() {
213-
const res = await fetch(
214-
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=5",
215-
);
216-
const json = await res.json();
217-
return json.posts as ChangelogItem[];
218-
}
219-
220-
type UserMetrics = {
221-
totalUsers: number;
222-
activeUsers: number;
223-
newUsers: number;
224-
returningUsers: number;
225-
};
226-
227-
type TimeSeriesMetrics = UserMetrics & {
228-
date: string;
229-
};
230-
231-
function processTimeSeriesData(
232-
userStats: WalletUserStats[],
233-
): TimeSeriesMetrics[] {
234-
const metrics: TimeSeriesMetrics[] = [];
235-
236-
let cumulativeUsers = 0;
237-
for (const stat of userStats) {
238-
cumulativeUsers += stat.newUsers ?? 0;
239-
metrics.push({
240-
date: stat.date,
241-
activeUsers: stat.totalUsers ?? 0,
242-
returningUsers: stat.returningUsers ?? 0,
243-
newUsers: stat.newUsers ?? 0,
244-
totalUsers: cumulativeUsers,
245-
});
246-
}
247-
248-
return metrics;
249-
}
250-
251-
function UsersChartCard({
252-
userStats,
253-
searchParams,
254-
}: {
255-
userStats: WalletUserStats[];
256-
searchParams?: { [key: string]: string | string[] | undefined };
257-
}) {
258-
const timeSeriesData = processTimeSeriesData(userStats);
259-
260-
const chartConfig = {
261-
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
262-
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
263-
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
264-
returningUsers: {
265-
label: "Returning Users",
266-
color: "hsl(var(--chart-4))",
267-
},
268-
} as const;
269-
270-
return (
271-
<CombinedBarChartCard
272-
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
273-
title="Users"
274-
chartConfig={chartConfig}
275-
activeChart={
276-
(searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers"
277-
}
278-
data={timeSeriesData}
279-
aggregateFn={(_data, key) =>
280-
timeSeriesData[timeSeriesData.length - 2]?.[key]
281-
}
282-
// Get the trend from the last two COMPLETE periods
283-
trendFn={(data, key) =>
284-
data.filter((d) => (d[key] as number) > 0).length >= 3
285-
? ((data[data.length - 2]?.[key] as number) ?? 0) /
286-
((data[data.length - 3]?.[key] as number) ?? 0) -
287-
1
288-
: undefined
289-
}
290-
queryKey="usersChart"
291-
existingQueryParams={searchParams}
292-
/>
293-
);
294-
}
295-
296-
async function WalletDistributionCard({ data }: { data: WalletStats[] }) {
297-
const formattedData = await Promise.all(
298-
data
299-
.filter((w) => w.walletType !== "smart" && w.walletType !== "smartWallet")
300-
.map(async (w) => {
301-
const wallet = await getWalletInfo(w.walletType as WalletId).catch(
302-
() => ({ name: w.walletType }),
303-
);
304-
return {
305-
walletType: w.walletType,
306-
uniqueWalletsConnected: w.uniqueWalletsConnected,
307-
totalConnections: w.totalConnections,
308-
walletName: wallet.name,
309-
};
310-
}),
311-
);
312-
313-
return (
314-
<PieChartCard
315-
title="Wallets Connected"
316-
data={formattedData.map(({ walletName, uniqueWalletsConnected }) => {
317-
return {
318-
value: uniqueWalletsConnected,
319-
label: walletName,
320-
};
321-
})}
322-
/>
323-
);
324-
}
325-
326-
function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) {
327-
return (
328-
<PieChartCard
329-
title="Social Authentication"
330-
data={data.map(({ authenticationMethod, uniqueWalletsConnected }) => ({
331-
value: uniqueWalletsConnected,
332-
label: authenticationMethod,
333-
}))}
334-
/>
335-
);
336-
}

0 commit comments

Comments
 (0)