Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ export interface WalletStats {
walletType: string;
}

export interface WalletUserStats {
date: string;
newUsers: number;
returningUsers: number;
totalUsers: number;
}

export interface InAppWalletStats {
date: string;
authenticationMethod: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer } from "stories/utils";
import { BarChart } from "./BarChart";

const meta = {
title: "project/Overview/BarChart",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Basic: Story = {
parameters: {
nextjs: {
appDirectory: true,
},
},
};

const chartConfig = {
views: {
label: "Daily Views",
color: "hsl(var(--chart-1))",
},
users: {
label: "Active Users",
color: "hsl(var(--chart-2))",
},
};

const generateDailyData = (days: number) => {
const data = [];
const today = new Date();

for (let i = days - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);

data.push({
date: date.toISOString(),
views: Math.floor(Math.random() * 1000) + 500,
users: Math.floor(Math.random() * 800) + 200,
});
}

return data;
};

function Component() {
return (
<div className="container max-w-[800px] space-y-8 py-8">
<BadgeContainer label="Views Data">
<BarChart
tooltipLabel="Daily Views"
chartConfig={chartConfig}
data={generateDailyData(14)}
activeKey="views"
/>
</BadgeContainer>

<BadgeContainer label="Users Data">
<BarChart
tooltipLabel="Active Users"
chartConfig={chartConfig}
data={generateDailyData(14)}
activeKey="users"
/>
</BadgeContainer>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { formatTickerNumber } from "lib/format-utils";
import {
Bar,
CartesianGrid,
BarChart as RechartsBarChart,
XAxis,
YAxis,
} from "recharts";

export function BarChart({
chartConfig,
data,
activeKey,
tooltipLabel,
}: {
chartConfig: ChartConfig;
data: { [key in string]: number | string }[];
activeKey: string;
tooltipLabel?: string;
}) {
return (
<ChartContainer
config={{
[activeKey]: {
label: tooltipLabel ?? chartConfig[activeKey]?.label,
},
...chartConfig,
}}
className="aspect-auto h-[250px] w-full pt-6"
>
<RechartsBarChart
accessibilityLayer
data={data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value: string) => {
const date = new Date(value);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}}
/>
<YAxis
width={32}
dataKey={activeKey}
tickLine={false}
axisLine={false}
tickFormatter={(value: number) => formatTickerNumber(value)}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey={activeKey}
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}}
/>
}
/>
<Bar
dataKey={activeKey}
radius={4}
fill={chartConfig[activeKey]?.color ?? "hsl(var(--chart-1))"}
/>
</RechartsBarChart>
</ChartContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { CombinedBarChartCard } from "./CombinedBarChartCard";

const meta = {
title: "project/Overview/CombinedBarChartCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Desktop: Story = {
parameters: {
nextjs: {
appDirectory: true,
},
},
};

export const Mobile: Story = {
parameters: {
nextjs: {
appDirectory: true,
},
viewport: mobileViewport("iphone14"),
},
};

const chartConfig = {
dailyUsers: {
label: "Daily Active Users",
color: "hsl(var(--chart-1))",
},
monthlyUsers: {
label: "Monthly Active Users",
color: "hsl(var(--chart-2))",
},
annualUsers: {
label: "Annual Active Users",
color: "hsl(var(--chart-3))",
},
};

const generateTimeSeriesData = (days: number) => {
const data = [];
const today = new Date();

let dailyBase = 1000;
let monthlyBase = 5000;
let annualBase = 30000;

for (let i = days - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);

// Add some random variation
const dailyVariation = Math.random() * 200 - 100;
const monthlyVariation = Math.random() * 500 - 250;
const annualVariation = Math.random() * 500 - 250;

// Trend upwards slightly
dailyBase += 10;
monthlyBase += 50;
annualBase += 50;

data.push({
date: date.toISOString(),
dailyUsers: Math.max(0, Math.round(dailyBase + dailyVariation)),
monthlyUsers: Math.max(0, Math.round(monthlyBase + monthlyVariation)),
annualUsers: Math.max(0, Math.round(annualBase + annualVariation)),
});
}

return data;
};

function Component() {
return (
<div className="max-w-[1000px] space-y-8 py-8 md:container">
<BadgeContainer label="Daily Users View">
<CombinedBarChartCard
title="User Activity"
chartConfig={chartConfig}
data={generateTimeSeriesData(30)}
activeChart="dailyUsers"
/>
</BadgeContainer>

<BadgeContainer label="Monthly Users View">
<CombinedBarChartCard
title="User Activity"
chartConfig={chartConfig}
data={generateTimeSeriesData(30)}
activeChart="monthlyUsers"
/>
</BadgeContainer>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { BarChart } from "./BarChart";
import { Stat } from "./Stat";

type CombinedBarChartConfig<K extends string> = {
[key in K]: { label: string; color: string };
};

export function CombinedBarChartCard<
T extends string,
K extends Exclude<T, "date">,
>({
title,
chartConfig,
data,
activeChart,
aggregateFn = (data, key) =>
data[data.length - 1]?.[key] as number | undefined,
trendFn = (data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 2
? ((data[data.length - 1]?.[key] as number) ?? 0) /
((data[data.length - 2]?.[key] as number) ?? 0) -
1
: undefined,
existingQueryParams,
}: {
title?: string;
chartConfig: CombinedBarChartConfig<K>;
data: { [key in T]: number | string }[];
activeChart: K;
aggregateFn?: (d: typeof data, key: K) => number | undefined;
trendFn?: (d: typeof data, key: K) => number | undefined;
existingQueryParams?: { [key: string]: string | string[] | undefined };
}) {
return (
<Card className="max-md:rounded-none">
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0">
{title && (
<div className="flex flex-1 flex-col justify-center gap-1 p-6">
<CardTitle className="font-semibold text-lg">{title}</CardTitle>
</div>
)}
<div className="max-md:no-scrollbar overflow-x-auto border-t">
<div className="flex flex-nowrap">
{Object.keys(chartConfig).map((chart: string) => {
const key = chart as K;
return (
<Link
href={{
query: {
...existingQueryParams,
usersChart: key,
},
}}
scroll={false}
key={chart}
data-active={activeChart === chart}
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"
>
<Stat
label={chartConfig[key].label}
value={aggregateFn(data, key) ?? "--"}
trend={trendFn(data, key) || undefined}
/>
<div
className="absolute right-0 bottom-0 left-0 h-0 bg-foreground transition-all duration-300 ease-out data-[active=true]:h-[3px]"
data-active={activeChart === chart}
/>
</Link>
);
})}
</div>
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6 sm:pl-0">
<BarChart
tooltipLabel={title}
chartConfig={chartConfig}
data={data}
activeKey={activeChart}
/>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ import walletsIcon from "../../../../../../public/assets/tw-icons/wallets.svg";

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