Add Connect analytics page in Teams layout (#4384)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the analytics feature for team projects.

### Detailed summary
- Updated links and routes related to analytics feature
- Renamed functions for clarity
- Refactored components for analytics visualization
- Added new UI components for analytics dashboard

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectSDKCard.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx`, `apps/dashboard/src/pages/dashboard/connect/analytics.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
MananTank
2024-09-04 19:52:29 +00:00
parent d254505436
commit d8f55efc97
17 changed files with 243 additions and 308 deletions
@@ -110,7 +110,7 @@ function ProjectCard(props: {
<Link
className="static group before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:z-0"
// remove /connect when we have overview page
href={`/team/${team_slug}/${project.slug}/connect/in-app-wallets`}
href={`/team/${team_slug}/${project.slug}/connect/analytics`}
>
<h2 className="text-base font-medium">{project.name}</h2>
</Link>
@@ -1,72 +1,21 @@
import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
import { CableIcon, WalletCardsIcon } from "lucide-react";
import type React from "react";
import { useMemo } from "react";
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
"use client";
import { useWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI";
export function ConnectAnalyticsDashboard(props: {
walletStats: WalletStats;
isLoading: boolean;
clientId: string;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.walletStats.timeSeries.reduce(
(acc, curr) => {
acc.totalWallets += curr.totalWallets;
acc.uniqueWallets += curr.uniqueWallets;
return acc;
},
{ uniqueWallets: 0, totalWallets: 0 },
);
}, [props.walletStats]);
const statsQuery = useWalletStats(props.clientId);
return (
<div className="flex flex-col gap-4 lg:gap-6">
{/* Summary Stat Cards */}
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
<Stat
label="Unique Wallets"
value={uniqueWallets}
icon={WalletCardsIcon}
/>
</div>
<DailyConnectionsChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
<WalletConnectorsChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
<WalletDistributionChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
</div>
<ConnectAnalyticsDashboardUI
walletStats={
statsQuery.data || {
timeSeries: [],
}
}
isLoading={statsQuery.isLoading}
/>
);
}
const Stat: React.FC<{
label: string;
value?: number;
icon: React.FC<{ className?: string }>;
}> = ({ label, value, icon: Icon }) => {
return (
<dl className="bg-muted/50 rounded-lg border border-border p-4 lg:p-6 flex items-center gap-4 justify-between">
<div>
<dd className="text-3xl lg:text-5xl font-semibold tracking-tight">
{value}
</dd>
<dt className="text-sm lg:text-lg font-medium text-muted-foreground tracking-tight">
{label}
</dt>
</div>
<Icon className="text-muted-foreground size-12 opacity-50 hidden lg:block" />
</dl>
);
};
@@ -0,0 +1,72 @@
import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
import { CableIcon, WalletCardsIcon } from "lucide-react";
import type React from "react";
import { useMemo } from "react";
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
export function ConnectAnalyticsDashboardUI(props: {
walletStats: WalletStats;
isLoading: boolean;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.walletStats.timeSeries.reduce(
(acc, curr) => {
acc.totalWallets += curr.totalWallets;
acc.uniqueWallets += curr.uniqueWallets;
return acc;
},
{ uniqueWallets: 0, totalWallets: 0 },
);
}, [props.walletStats]);
return (
<div className="flex flex-col gap-4 lg:gap-6">
{/* Summary Stat Cards */}
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
<Stat
label="Unique Wallets"
value={uniqueWallets}
icon={WalletCardsIcon}
/>
</div>
<DailyConnectionsChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
<WalletConnectorsChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
<WalletDistributionChartCard
walletStats={props.walletStats}
isLoading={props.isLoading}
/>
</div>
);
}
const Stat: React.FC<{
label: string;
value?: number;
icon: React.FC<{ className?: string }>;
}> = ({ label, value, icon: Icon }) => {
return (
<dl className="bg-muted/50 rounded-lg border border-border p-4 lg:p-6 flex items-center gap-4 justify-between">
<div>
<dd className="text-3xl lg:text-5xl font-semibold tracking-tight">
{value}
</dd>
<dt className="text-sm lg:text-lg font-medium text-muted-foreground tracking-tight">
{label}
</dt>
</div>
<Icon className="text-muted-foreground size-12 opacity-50 hidden lg:block" />
</dl>
);
};
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../../../../../stories/utils";
import { ConnectAnalyticsDashboard } from "../ConnectAnalyticsDashboard";
import { createWalletStatusStub } from "./storyUtils";
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Analytics Dashboard",
@@ -28,8 +28,8 @@ export const Mobile: Story = {
function Component() {
return (
<div className="min-h-screen bg-background p-4 text-foreground gap-6 max-w-[1000px] flex flex-col mx-auto py-10">
<ConnectAnalyticsDashboard
walletStats={createWalletStatusStub(30)}
<ConnectAnalyticsDashboardUI
walletStats={createWalletStatsStub(30)}
isLoading={false}
/>
</div>
@@ -0,0 +1,85 @@
import { SiReact } from "@react-icons/all-files/si/SiReact";
import { SiTypescript } from "@react-icons/all-files/si/SiTypescript";
import { SiUnity } from "@react-icons/all-files/si/SiUnity";
import Link from "next/link";
import { SiUnrealengine } from "react-icons/si";
import { SiDotnet } from "react-icons/si";
export function ConnectSDKCard() {
return (
<div className="border border-border bg-muted/50 rounded-lg p-6 relative">
<h3 className="text-2xl font-semibold tracking-tight mb-1">
Connect SDK
</h3>
<p className="mb-8 text-muted-foreground text-sm">
Add the Connect SDK to your app to start collecting analytics.
</p>
<div className="grid gap-6 md:gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 max-w-[500px]">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/getting-started"
icon={SiTypescript}
label="TypeScript SDK"
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/getting-started"
icon={SiReact}
label="React SDK"
/>
<DocLink
link="https://portal.thirdweb.com/react-native/v5/getting-started"
icon={SiReact}
label="React Native SDK"
/>
<DocLink
link="https://portal.thirdweb.com/unity/v4/getting-started"
icon={SiUnity}
label="Unity SDK"
/>
<DocLink
link="https://portal.thirdweb.com/unreal/getting-started"
icon={SiUnrealengine}
label="Unreal SDK"
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
icon={SiDotnet}
label=".NET SDK"
/>
</div>
<BackgroundPattern />
</div>
);
}
function BackgroundPattern() {
const color = "hsl(var(--foreground)/50%)";
return (
<div
className="hidden xl:block absolute w-[50%] right-2 top-4 bottom-4 z-[1]"
style={{
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
maskImage: "linear-gradient(to left, black, transparent)",
}}
/>
);
}
function DocLink(props: {
link: string;
label: string;
icon: React.FC<{ className?: string }>;
}) {
return (
<Link
href={props.link}
target="_blank"
className="text-muted-foreground hover:text-foreground flex items-center gap-2 text-sm"
>
<props.icon className="size-4" />
{props.label}
</Link>
);
}
@@ -4,7 +4,7 @@ import {
mobileViewport,
} from "../../../../../../../stories/utils";
import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
import { createWalletStatusStub } from "./storyUtils";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/DailyConnections",
@@ -33,35 +33,35 @@ function Component() {
<div className="min-h-screen bg-background p-4 text-foreground gap-6 max-w-[1000px] flex flex-col mx-auto">
<BadgeContainer label="30 days">
<DailyConnectionsChartCard
walletStats={createWalletStatusStub(30)}
walletStats={createWalletStatsStub(30)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<DailyConnectionsChartCard
walletStats={createWalletStatusStub(60)}
walletStats={createWalletStatsStub(60)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<DailyConnectionsChartCard
walletStats={createWalletStatusStub(10)}
walletStats={createWalletStatsStub(10)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<DailyConnectionsChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DailyConnectionsChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={true}
/>
</BadgeContainer>
@@ -1,3 +1,5 @@
"use client";
import {
type ChartConfig,
ChartContainer,
@@ -1,3 +1,5 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import { useMemo } from "react";
@@ -4,7 +4,7 @@ import {
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletConnectorsChartCard } from "./WalletConnectorsChartCard";
import { createWalletStatusStub } from "./storyUtils";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Connectors",
@@ -33,35 +33,35 @@ function Component() {
<div className="min-h-screen bg-background p-4 text-foreground gap-6 max-w-[1000px] flex flex-col mx-auto">
<BadgeContainer label="30 days">
<WalletConnectorsChartCard
walletStats={createWalletStatusStub(30)}
walletStats={createWalletStatsStub(30)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletConnectorsChartCard
walletStats={createWalletStatusStub(60)}
walletStats={createWalletStatsStub(60)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletConnectorsChartCard
walletStats={createWalletStatusStub(10)}
walletStats={createWalletStatsStub(10)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletConnectorsChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletConnectorsChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={true}
/>
</BadgeContainer>
@@ -4,7 +4,7 @@ import {
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletDistributionChartCard } from "./WalletDistributionChartCard";
import { createWalletStatusStub } from "./storyUtils";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Distribution",
@@ -33,35 +33,35 @@ function Component() {
<div className="min-h-screen bg-background p-4 text-foreground gap-6 max-w-[1000px] flex flex-col mx-auto">
<BadgeContainer label="30 days">
<WalletDistributionChartCard
walletStats={createWalletStatusStub(30)}
walletStats={createWalletStatsStub(30)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletDistributionChartCard
walletStats={createWalletStatusStub(60)}
walletStats={createWalletStatsStub(60)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletDistributionChartCard
walletStats={createWalletStatusStub(10)}
walletStats={createWalletStatsStub(10)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletDistributionChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletDistributionChartCard
walletStats={createWalletStatusStub(0)}
walletStats={createWalletStatsStub(0)}
isLoading={true}
/>
</BadgeContainer>
@@ -21,7 +21,7 @@ const pickRandomWallet = () => {
];
};
export function createWalletStatusStub(days: number): WalletStats {
export function createWalletStatsStub(days: number): WalletStats {
const timeSeries: WalletStats["timeSeries"] = [];
let d = days;
@@ -1,9 +1,38 @@
export default function Page() {
import { getProject } from "@/api/projects";
import { notFound } from "next/navigation";
import { ConnectAnalyticsDashboard } from "./ConnectAnalyticsDashboard";
import { ConnectSDKCard } from "./_components/ConnectSDKCard";
export default async function Page(props: {
params: {
team_slug: string;
project_slug: string;
};
}) {
const project = await getProject(
props.params.team_slug,
props.params.project_slug,
);
if (!project) {
return notFound();
}
return (
<div className="h-full py-6 container flex items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
Analytics
</h1>
<div className="pb-10 max-sm:pt-6">
<div>
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight mb-1">
Connect Analytics
</h1>
<p className="text-muted-foreground text-sm md:text-base">
Visualize how your users are connecting to your app
</p>
</div>
<div className="h-6 lg:h-8" />
<ConnectAnalyticsDashboard clientId={project.publishableKey} />
<div className="h-4 lg:h-8" />
<ConnectSDKCard />
</div>
);
}
@@ -11,14 +11,14 @@ export default function Layout(props: {
const { team_slug, project_slug } = props.params;
const links: { label: string; href: string }[] = [
{
label: "In-App Wallets",
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
},
{
label: "Analytics",
href: `/team/${team_slug}/${project_slug}/connect/analytics`,
},
{
label: "In-App Wallets",
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
},
{
label: "Ecosystem Wallets",
href: `/team/${team_slug}/${project_slug}/connect/ecosystem`,
@@ -7,5 +7,5 @@ export default function Page(props: {
};
}) {
const { team_slug, project_slug } = props.params;
redirect(`/team/${team_slug}/${project_slug}/connect/in-app-wallets`);
redirect(`/team/${team_slug}/${project_slug}/connect/analytics`);
}
@@ -44,7 +44,7 @@ export default async function TeamLayout(props: {
<TeamTabs
links={[
{
href: `/team/${props.params.team_slug}/${props.params.project_slug}/connect`,
href: `/team/${props.params.team_slug}/${props.params.project_slug}/connect/analytics`,
name: "Connect",
},
{
@@ -1,109 +0,0 @@
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { Box, Flex, HStack, Icon, useColorModeValue } from "@chakra-ui/react";
import Image from "next/image";
import type { ReactNode } from "react";
import { FiLock } from "react-icons/fi";
import { Card, Heading, Text, TrackedLinkButton } from "tw-components";
interface GatedFeatureProps {
children: ReactNode;
title?: string;
description?: ReactNode;
imgSrc?: string;
imgWidth?: number;
imgHeight?: number;
trackingLabel?: string;
}
export const GatedFeature: React.FC<GatedFeatureProps> = ({
children,
title,
description,
imgSrc,
imgWidth,
imgHeight,
trackingLabel,
}) => {
const meQuery = useAccount();
const { data: account } = meQuery;
const bg = useColorModeValue("backgroundCardHighlight", "transparent");
if (!account) {
return null;
}
if (account.advancedEnabled) {
return children;
}
if (!title) {
return null;
}
return (
<Card
pt={{ base: 4, lg: 8 }}
pb={{ base: 8, lg: 8 }}
px={{ base: 4, lg: 8, xl: 16 }}
bg={bg}
>
<Flex
flexDir={{ base: "column-reverse", lg: "row" }}
justifyContent="space-between"
alignItems="center"
gap={8}
>
<Flex
flexDir="column"
gap={6}
alignItems={{ base: "center", lg: "flex-start" }}
>
<Flex flexDir="column" gap={2}>
<HStack
gap={2}
alignItems="center"
justifyContent={{ base: "center", lg: "flex-start" }}
>
<Icon as={FiLock} opacity={0.3} boxSize={4} />
<Text size="label.md" opacity={0.5}>
Advanced feature
</Text>
</HStack>
<Heading size="title.md" textAlign={{ base: "center", lg: "left" }}>
{title}
</Heading>
</Flex>
<Text size="body.lg" textAlign={{ base: "center", lg: "left" }}>
{description}
</Text>
<Box>
<TrackedLinkButton
colorScheme="primary"
category="advancedFeature"
label={trackingLabel}
href={"/dashboard/settings/billing"}
borderRadius="lg"
py={6}
px={6}
>
Upgrade to Growth
</TrackedLinkButton>
</Box>
</Flex>
{imgSrc && (
<Box overflow="hidden">
<Image
src={imgSrc}
width={imgWidth}
height={imgHeight}
alt={trackingLabel ?? ""}
/>
</Box>
)}
</Flex>
</Card>
);
};
@@ -1,13 +1,6 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {
type ApiKey,
useApiKeys,
useWalletStats,
} from "@3rdweb-sdk/react/hooks/useApi";
import { type ApiKey, useApiKeys } from "@3rdweb-sdk/react/hooks/useApi";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { SiReact } from "@react-icons/all-files/si/SiReact";
import { SiTypescript } from "@react-icons/all-files/si/SiTypescript";
import { SiUnity } from "@react-icons/all-files/si/SiUnity";
import { AppLayout } from "components/app-layouts/app";
import { ApiKeysMenu } from "components/settings/ApiKeys/Menu";
import { ConnectSidebar } from "core-ui/sidebar/connect";
@@ -15,11 +8,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { PageId } from "page-id";
import { useMemo, useState } from "react";
import { SiUnrealengine } from "react-icons/si";
import { SiDotnet } from "react-icons/si";
import type { ThirdwebNextPage } from "utils/types";
import { Button } from "../../../@/components/ui/button";
import { ConnectAnalyticsDashboard } from "../../../app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard";
import { ConnectSDKCard } from "../../../app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectSDKCard";
const DashboardConnectAnalytics: ThirdwebNextPage = () => {
const router = useRouter();
@@ -46,7 +38,6 @@ const DashboardConnectAnalytics: ThirdwebNextPage = () => {
return undefined;
}, [apiKeys, defaultClientId, selectedKey_]);
const statsQuery = useWalletStats(selectedKey?.key);
const showLoader = loggedInUser.isLoading || keysQuery.isLoading;
return (
@@ -82,14 +73,7 @@ const DashboardConnectAnalytics: ThirdwebNextPage = () => {
) : (
<>
{selectedKey ? (
<ConnectAnalyticsDashboard
walletStats={
statsQuery.data || {
timeSeries: [],
}
}
isLoading={statsQuery.isLoading}
/>
<ConnectAnalyticsDashboard clientId={selectedKey.key} />
) : (
<NoAPIFoundCard />
)}
@@ -97,7 +81,7 @@ const DashboardConnectAnalytics: ThirdwebNextPage = () => {
)}
<div className="h-4 lg:h-8" />
<SDKCtaSection />
<ConnectSDKCard />
</div>
);
};
@@ -116,85 +100,6 @@ function NoAPIFoundCard() {
);
}
function SDKCtaSection() {
return (
<div className="border border-border bg-muted/50 rounded-lg p-6 relative">
<h3 className="text-2xl font-semibold tracking-tight mb-1">
Connect SDK
</h3>
<p className="mb-8 text-muted-foreground text-sm">
Add the Connect SDK to your app to start collecting analytics.
</p>
<div className="grid gap-6 md:gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 max-w-[500px]">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/getting-started"
icon={SiTypescript}
label="TypeScript SDK"
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/getting-started"
icon={SiReact}
label="React SDK"
/>
<DocLink
link="https://portal.thirdweb.com/react-native/v5/getting-started"
icon={SiReact}
label="React Native SDK"
/>
<DocLink
link="https://portal.thirdweb.com/unity/v4/getting-started"
icon={SiUnity}
label="Unity SDK"
/>
<DocLink
link="https://portal.thirdweb.com/unreal/getting-started"
icon={SiUnrealengine}
label="Unreal SDK"
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
icon={SiDotnet}
label=".NET SDK"
/>
</div>
<BackgroundPattern />
</div>
);
}
function BackgroundPattern() {
const color = "hsl(var(--foreground)/50%)";
return (
<div
className="hidden xl:block absolute w-[50%] right-2 top-4 bottom-4 z-[1]"
style={{
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
maskImage: "linear-gradient(to left, black, transparent)",
}}
/>
);
}
function DocLink(props: {
link: string;
label: string;
icon: React.FC<{ className?: string }>;
}) {
return (
<Link
href={props.link}
target="_blank"
className="text-muted-foreground hover:text-foreground flex items-center gap-2 text-sm"
>
<props.icon className="size-4" />
{props.label}
</Link>
);
}
DashboardConnectAnalytics.getLayout = (page, props) => (
<AppLayout {...props} hasSidebar={true}>
<ConnectSidebar activePage="analytics" />