Skip to content

Commit 4d975f6

Browse files
committed
[Dashboard] Feature: Project Overview (Analytics) Page (#5340)
CNCT-2182 CNCT-2180 CNCT-2177 CNCT-2179 CNCT-2178 CNCT-2176 https://github.com/user-attachments/assets/5c676ec4-ca2f-4796-880a-944636f9f0d5 <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the analytics and user statistics features in the dashboard. It introduces new components, updates existing ones, and improves the handling of wallet and user data, including stats visualization and data fetching. ### Detailed summary - Added `WalletUserStats` interface to manage user statistics. - Updated `ProjectOverviewHeader` to accept `interval` and `range` props. - Introduced new `Stat` component for displaying metrics with trends. - Created `PieChart`, `BarChart`, and `CombinedBarChartCard` components for visualizing data. - Implemented `StatBreakdownCard` for detailed metric breakdowns. - Enhanced `UsersChartCard` to process and display user statistics. - Updated `PageProps` and data fetching functions for improved analytics handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 1db950e commit 4d975f6

18 files changed

+1496
-20
lines changed

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ export interface WalletStats {
238238
walletType: string;
239239
}
240240

241+
export interface WalletUserStats {
242+
date: string;
243+
newUsers: number;
244+
returningUsers: number;
245+
totalUsers: number;
246+
}
247+
241248
export interface InAppWalletStats {
242249
date: string;
243250
authenticationMethod: string;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { BadgeContainer } from "stories/utils";
3+
import { BarChart } from "./BarChart";
4+
5+
const meta = {
6+
title: "project/Overview/BarChart",
7+
component: Component,
8+
parameters: {
9+
layout: "centered",
10+
},
11+
} satisfies Meta<typeof Component>;
12+
13+
export default meta;
14+
type Story = StoryObj<typeof meta>;
15+
16+
export const Basic: Story = {
17+
parameters: {
18+
nextjs: {
19+
appDirectory: true,
20+
},
21+
},
22+
};
23+
24+
const chartConfig = {
25+
views: {
26+
label: "Daily Views",
27+
color: "hsl(var(--chart-1))",
28+
},
29+
users: {
30+
label: "Active Users",
31+
color: "hsl(var(--chart-2))",
32+
},
33+
};
34+
35+
const generateDailyData = (days: number) => {
36+
const data = [];
37+
const today = new Date();
38+
39+
for (let i = days - 1; i >= 0; i--) {
40+
const date = new Date(today);
41+
date.setDate(date.getDate() - i);
42+
43+
data.push({
44+
date: date.toISOString(),
45+
views: Math.floor(Math.random() * 1000) + 500,
46+
users: Math.floor(Math.random() * 800) + 200,
47+
});
48+
}
49+
50+
return data;
51+
};
52+
53+
function Component() {
54+
return (
55+
<div className="container max-w-[800px] space-y-8 py-8">
56+
<BadgeContainer label="Views Data">
57+
<BarChart
58+
tooltipLabel="Daily Views"
59+
chartConfig={chartConfig}
60+
data={generateDailyData(14)}
61+
activeKey="views"
62+
/>
63+
</BadgeContainer>
64+
65+
<BadgeContainer label="Users Data">
66+
<BarChart
67+
tooltipLabel="Active Users"
68+
chartConfig={chartConfig}
69+
data={generateDailyData(14)}
70+
activeKey="users"
71+
/>
72+
</BadgeContainer>
73+
</div>
74+
);
75+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"use client";
2+
import {
3+
type ChartConfig,
4+
ChartContainer,
5+
ChartTooltip,
6+
ChartTooltipContent,
7+
} from "@/components/ui/chart";
8+
import { formatTickerNumber } from "lib/format-utils";
9+
import {
10+
Bar,
11+
CartesianGrid,
12+
BarChart as RechartsBarChart,
13+
XAxis,
14+
YAxis,
15+
} from "recharts";
16+
17+
export function BarChart({
18+
chartConfig,
19+
data,
20+
activeKey,
21+
tooltipLabel,
22+
}: {
23+
chartConfig: ChartConfig;
24+
data: { [key in string]: number | string }[];
25+
activeKey: string;
26+
tooltipLabel?: string;
27+
}) {
28+
return (
29+
<ChartContainer
30+
config={{
31+
[activeKey]: {
32+
label: tooltipLabel ?? chartConfig[activeKey]?.label,
33+
},
34+
...chartConfig,
35+
}}
36+
className="aspect-auto h-[250px] w-full pt-6"
37+
>
38+
<RechartsBarChart
39+
accessibilityLayer
40+
data={data}
41+
margin={{
42+
left: 12,
43+
right: 12,
44+
}}
45+
>
46+
<CartesianGrid vertical={false} />
47+
<XAxis
48+
dataKey="date"
49+
tickLine={false}
50+
axisLine={false}
51+
tickMargin={8}
52+
minTickGap={32}
53+
tickFormatter={(value: string) => {
54+
const date = new Date(value);
55+
return date.toLocaleDateString("en-US", {
56+
month: "short",
57+
day: "numeric",
58+
});
59+
}}
60+
/>
61+
<YAxis
62+
width={32}
63+
dataKey={activeKey}
64+
tickLine={false}
65+
axisLine={false}
66+
tickFormatter={(value: number) => formatTickerNumber(value)}
67+
/>
68+
<ChartTooltip
69+
content={
70+
<ChartTooltipContent
71+
className="w-[150px]"
72+
nameKey={activeKey}
73+
labelFormatter={(value) => {
74+
return new Date(value).toLocaleDateString("en-US", {
75+
month: "short",
76+
day: "numeric",
77+
year: "numeric",
78+
});
79+
}}
80+
/>
81+
}
82+
/>
83+
<Bar
84+
dataKey={activeKey}
85+
radius={4}
86+
fill={chartConfig[activeKey]?.color ?? "hsl(var(--chart-1))"}
87+
/>
88+
</RechartsBarChart>
89+
</ChartContainer>
90+
);
91+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { BadgeContainer, mobileViewport } from "stories/utils";
3+
import { CombinedBarChartCard } from "./CombinedBarChartCard";
4+
5+
const meta = {
6+
title: "project/Overview/CombinedBarChartCard",
7+
component: Component,
8+
parameters: {
9+
layout: "centered",
10+
},
11+
} satisfies Meta<typeof Component>;
12+
13+
export default meta;
14+
type Story = StoryObj<typeof meta>;
15+
16+
export const Desktop: Story = {
17+
parameters: {
18+
nextjs: {
19+
appDirectory: true,
20+
},
21+
},
22+
};
23+
24+
export const Mobile: Story = {
25+
parameters: {
26+
nextjs: {
27+
appDirectory: true,
28+
},
29+
viewport: mobileViewport("iphone14"),
30+
},
31+
};
32+
33+
const chartConfig = {
34+
dailyUsers: {
35+
label: "Daily Active Users",
36+
color: "hsl(var(--chart-1))",
37+
},
38+
monthlyUsers: {
39+
label: "Monthly Active Users",
40+
color: "hsl(var(--chart-2))",
41+
},
42+
annualUsers: {
43+
label: "Annual Active Users",
44+
color: "hsl(var(--chart-3))",
45+
},
46+
};
47+
48+
const generateTimeSeriesData = (days: number) => {
49+
const data = [];
50+
const today = new Date();
51+
52+
let dailyBase = 1000;
53+
let monthlyBase = 5000;
54+
let annualBase = 30000;
55+
56+
for (let i = days - 1; i >= 0; i--) {
57+
const date = new Date(today);
58+
date.setDate(date.getDate() - i);
59+
60+
// Add some random variation
61+
const dailyVariation = Math.random() * 200 - 100;
62+
const monthlyVariation = Math.random() * 500 - 250;
63+
const annualVariation = Math.random() * 500 - 250;
64+
65+
// Trend upwards slightly
66+
dailyBase += 10;
67+
monthlyBase += 50;
68+
annualBase += 50;
69+
70+
data.push({
71+
date: date.toISOString(),
72+
dailyUsers: Math.max(0, Math.round(dailyBase + dailyVariation)),
73+
monthlyUsers: Math.max(0, Math.round(monthlyBase + monthlyVariation)),
74+
annualUsers: Math.max(0, Math.round(annualBase + annualVariation)),
75+
});
76+
}
77+
78+
return data;
79+
};
80+
81+
function Component() {
82+
return (
83+
<div className="max-w-[1000px] space-y-8 py-8 md:container">
84+
<BadgeContainer label="Daily Users View">
85+
<CombinedBarChartCard
86+
title="User Activity"
87+
chartConfig={chartConfig}
88+
data={generateTimeSeriesData(30)}
89+
activeChart="dailyUsers"
90+
/>
91+
</BadgeContainer>
92+
93+
<BadgeContainer label="Monthly Users View">
94+
<CombinedBarChartCard
95+
title="User Activity"
96+
chartConfig={chartConfig}
97+
data={generateTimeSeriesData(30)}
98+
activeChart="monthlyUsers"
99+
/>
100+
</BadgeContainer>
101+
</div>
102+
);
103+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2+
import Link from "next/link";
3+
import { BarChart } from "./BarChart";
4+
import { Stat } from "./Stat";
5+
6+
type CombinedBarChartConfig<K extends string> = {
7+
[key in K]: { label: string; color: string };
8+
};
9+
10+
export function CombinedBarChartCard<
11+
T extends string,
12+
K extends Exclude<T, "date">,
13+
>({
14+
title,
15+
chartConfig,
16+
data,
17+
activeChart,
18+
aggregateFn = (data, key) =>
19+
data[data.length - 1]?.[key] as number | undefined,
20+
trendFn = (data, key) =>
21+
data.filter((d) => (d[key] as number) > 0).length >= 2
22+
? ((data[data.length - 1]?.[key] as number) ?? 0) /
23+
((data[data.length - 2]?.[key] as number) ?? 0) -
24+
1
25+
: undefined,
26+
existingQueryParams,
27+
}: {
28+
title?: string;
29+
chartConfig: CombinedBarChartConfig<K>;
30+
data: { [key in T]: number | string }[];
31+
activeChart: K;
32+
aggregateFn?: (d: typeof data, key: K) => number | undefined;
33+
trendFn?: (d: typeof data, key: K) => number | undefined;
34+
existingQueryParams?: { [key: string]: string | string[] | undefined };
35+
}) {
36+
return (
37+
<Card className="max-md:rounded-none">
38+
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0">
39+
{title && (
40+
<div className="flex flex-1 flex-col justify-center gap-1 p-6">
41+
<CardTitle className="font-semibold text-lg">{title}</CardTitle>
42+
</div>
43+
)}
44+
<div className="max-md:no-scrollbar overflow-x-auto border-t">
45+
<div className="flex flex-nowrap">
46+
{Object.keys(chartConfig).map((chart: string) => {
47+
const key = chart as K;
48+
return (
49+
<Link
50+
href={{
51+
query: {
52+
...existingQueryParams,
53+
usersChart: key,
54+
},
55+
}}
56+
scroll={false}
57+
key={chart}
58+
data-active={activeChart === chart}
59+
className="relative z-30 flex min-w-[200px] flex-1 flex-col justify-center gap-1 border-l first:border-l-none hover:bg-muted/50"
60+
>
61+
<Stat
62+
label={chartConfig[key].label}
63+
value={aggregateFn(data, key) ?? "--"}
64+
trend={trendFn(data, key) || undefined}
65+
/>
66+
<div
67+
className="absolute right-0 bottom-0 left-0 h-0 bg-foreground transition-all duration-300 ease-out data-[active=true]:h-[3px]"
68+
data-active={activeChart === chart}
69+
/>
70+
</Link>
71+
);
72+
})}
73+
</div>
74+
</div>
75+
</CardHeader>
76+
<CardContent className="px-2 sm:p-6 sm:pl-0">
77+
<BarChart
78+
tooltipLabel={title}
79+
chartConfig={chartConfig}
80+
data={data}
81+
activeKey={activeChart}
82+
/>
83+
</CardContent>
84+
</Card>
85+
);
86+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ import walletsIcon from "../../../../../../public/assets/tw-icons/wallets.svg";
1717

1818
export function EmptyState() {
1919
return (
20-
<div className="container flex items-center justify-center p-6 md:h-[770px]">
20+
<div className="flex items-center justify-center md:min-h-[500px]">
2121
<div className="group container flex flex-col items-center justify-center gap-8 rounded-lg border bg-card p-6 py-24">
2222
<div className="flex max-w-[500px] flex-col items-center justify-center gap-6">
2323
<AnimatedIcons />
2424
<div className="flex flex-col gap-0.5 text-center">
2525
<h3 className="font-semibold text-2xl text-foreground">
26-
Project Overview is Coming Soon
26+
Get Started with the Connect SDK
2727
</h3>
2828
<p className="text-base text-muted-foreground">
29-
Understand how users are interacting with your project
29+
Add the Connect SDK to your app to start collecting analytics.
3030
</p>
3131
</div>
3232
<div className="flex flex-wrap items-center justify-center gap-2">

0 commit comments

Comments
 (0)