diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts index caadf46dfe1..2ca4f9236cc 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts @@ -380,43 +380,6 @@ export function useAccountCredits() { }); } -async function getWalletUsage(args: { - clientId: string; - from?: Date; - to?: Date; - period?: "day" | "week" | "month" | "year" | "all"; -}) { - const { clientId, from, to, period } = args; - - const searchParams = new URLSearchParams(); - searchParams.append("clientId", clientId); - if (from) { - searchParams.append("from", from.toISOString()); - } - if (to) { - searchParams.append("to", to.toISOString()); - } - if (period) { - searchParams.append("period", period); - } - const res = await fetch( - `${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, - ); - const json = await res.json(); - - if (res.status !== 200) { - throw new Error(json.message); - } - - return json.data; -} - async function getUserOpUsage(args: { clientId: string; from?: Date; @@ -454,43 +417,6 @@ async function getUserOpUsage(args: { return json.data; } -export async function getInAppWalletUsage(args: { - clientId: string; - from?: Date; - to?: Date; - period?: "day" | "week" | "month" | "year" | "all"; -}) { - const { clientId, from, to, period } = args; - - const searchParams = new URLSearchParams(); - searchParams.append("clientId", clientId); - if (from) { - searchParams.append("from", from.toISOString()); - } - if (to) { - searchParams.append("to", to.toISOString()); - } - if (period) { - searchParams.append("period", period); - } - const res = await fetch( - `${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, - ); - const json = await res?.json(); - - if (!res || res.status !== 200) { - throw new Error(json.message); - } - - return json.data; -} - export function useUserOpUsageAggregate(args: { clientId: string; from?: Date; @@ -574,63 +500,6 @@ export function useUserOpUsagePeriod(args: { }); } -export function useWalletUsageAggregate(args: { - clientId: string; - from?: Date; - to?: Date; -}) { - const { clientId, from, to } = args; - const { user, isLoggedIn } = useLoggedInUser(); - - return useQuery({ - queryKey: accountKeys.walletStats( - user?.address as string, - clientId as string, - from?.toISOString() || "", - to?.toISOString() || "", - "all", - ), - queryFn: async () => { - return getWalletUsage({ - clientId, - from, - to, - period: "all", - }); - }, - enabled: !!clientId && !!user?.address && isLoggedIn, - }); -} - -export function useWalletUsagePeriod(args: { - clientId: string; - from?: Date; - to?: Date; - period: "day" | "week" | "month" | "year"; -}) { - const { clientId, from, to, period } = args; - const { user, isLoggedIn } = useLoggedInUser(); - - return useQuery({ - queryKey: accountKeys.walletStats( - user?.address as string, - clientId as string, - from?.toISOString() || "", - to?.toISOString() || "", - period, - ), - queryFn: async () => { - return getWalletUsage({ - clientId, - from, - to, - period, - }); - }, - enabled: !!clientId && !!user?.address && isLoggedIn, - }); -} - export function useUpdateAccount() { const { user } = useLoggedInUser(); const queryClient = useQueryClient(); diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx index 69375c6017a..a2979962263 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx @@ -59,7 +59,7 @@ export function BarChart({ }} /> { return new Date(value).toLocaleDateString("en-US", { @@ -77,6 +77,7 @@ export function BarChart({ year: "numeric", }); }} + valueFormatter={(v: unknown) => formatTickerNumber(v as number)} /> } /> diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx index f904ad08c84..b7602d9666c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx @@ -53,6 +53,7 @@ export function CombinedBarChartCard< usersChart: key, }, }} + prefetch scroll={false} key={chart} data-active={activeChart === chart} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx index ccce7e4028f..395b182a0dd 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx @@ -6,6 +6,7 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; +import { formatTickerNumber } from "lib/format-utils"; import { Pie, PieChart as RechartsPieChart } from "recharts"; export function PieChart({ @@ -39,7 +40,12 @@ export function PieChart({ } + content={ + formatTickerNumber(v as number)} + /> + } /> diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx index fa2a9032545..2bcd002d94c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx @@ -28,7 +28,9 @@ export function PieChartCard({ const sorted = [...data].sort((a, b) => b.value - a.value); // Take top 9 - const top10 = sorted.slice(0, 9); + const top10 = sorted.slice(0, 9).map((item) => ({ + ...item, + })); // Aggregate the rest const otherValue = sorted @@ -44,7 +46,11 @@ export function PieChartCard({ } return top10; - })(); + })().map((item, index) => ({ + ...item, + fill: item.fill || `hsl(var(--chart-${index + 1}))`, + })); + return ( diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx deleted file mode 100644 index 73b9c24ae76..00000000000 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; -import { - getInAppWalletUsage, - useUserOpUsageAggregate, - useWalletUsageAggregate, - useWalletUsagePeriod, -} from "@3rdweb-sdk/react/hooks/useApi"; -import { useQuery } from "@tanstack/react-query"; -import { - type Range, - getLastNDaysRange, -} from "components/analytics/date-range-selector"; -import { differenceInDays } from "date-fns"; -import { useState } from "react"; -import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI"; - -export function ConnectAnalyticsDashboard(props: { - clientId: string; - connectLayoutSlug: string; -}) { - const [range, setRange] = useState(() => - getLastNDaysRange("last-120"), - ); - - // use date-fns to calculate the number of days in the range - const daysInRange = differenceInDays(range.to, range.from); - const [intervalType, setIntervalType] = useState<"day" | "week">( - daysInRange > 30 ? "week" : "day", - ); - - const walletUsageQuery = useWalletUsagePeriod({ - clientId: props.clientId, - from: range.from, - to: range.to, - period: intervalType, - }); - - const walletUsageAggregateQuery = useWalletUsageAggregate({ - clientId: props.clientId, - from: range.from, - to: range.to, - }); - - const userOpAggregateQuery = useUserOpUsageAggregate({ - clientId: props.clientId, - }); - - const inAppAggregateQuery = useQuery({ - queryKey: ["in-app-usage-aggregate", props.clientId], - queryFn: async () => { - const [allTimeStats, monthlyStats] = await Promise.all([ - getInAppWalletUsage({ - clientId: props.clientId, - from: new Date(2022, 0, 1), - to: new Date(), - period: "all", - }), - getInAppWalletUsage({ - clientId: props.clientId, - from: new Date(new Date().getFullYear(), new Date().getMonth(), 1), - to: new Date(), - period: "month", - }), - ]); - return { allTimeStats, monthlyStats }; - }, - }); - - return ( -
- -
- ); -} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/layout.tsx index 7b233823438..81bf85e75eb 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/layout.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/layout.tsx @@ -12,11 +12,6 @@ export default async function Layout(props: { const { team_slug, project_slug } = await props.params; const links: SidebarLink[] = [ - { - label: "Analytics", - href: `/team/${team_slug}/${project_slug}/connect`, - exactMatch: true, - }, { label: "In-App Wallets", href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`, diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx index 67a91ba597d..e8c8a734c52 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx @@ -1,7 +1,5 @@ import { getProject } from "@/api/projects"; -import { ConnectSDKCard } from "components/shared/ConnectSDKCard"; -import { notFound } from "next/navigation"; -import { ConnectAnalyticsDashboard } from "./analytics/ConnectAnalyticsDashboard"; +import { notFound, redirect } from "next/navigation"; export default async function Page(props: { params: Promise<{ @@ -16,23 +14,7 @@ export default async function Page(props: { notFound(); } - return ( -
-
-

- Connect Analytics -

-

- Visualize how your users are connecting to your app -

-
-
- -
- -
+ redirect( + `/team/${params.team_slug}/${params.project_slug}/connect/in-app-wallets`, ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx index 79f129bdcd9..e25257be171 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx @@ -181,7 +181,7 @@ function processTimeSeriesData( let cumulativeUsers = 0; for (const stat of userStats) { - cumulativeUsers += stat.totalUsers ?? 0; + cumulativeUsers += stat.newUsers ?? 0; metrics.push({ date: stat.date, activeUsers: stat.totalUsers ?? 0, @@ -224,6 +224,14 @@ function UsersChartCard({ aggregateFn={(_data, key) => timeSeriesData[timeSeriesData.length - 1]?.[key] } + // Get the trend from the last two COMPLETE periods + trendFn={(data, key) => + data.filter((d) => (d[key] as number) > 0).length >= 3 + ? ((data[data.length - 2]?.[key] as number) ?? 0) / + ((data[data.length - 3]?.[key] as number) ?? 0) - + 1 + : undefined + } existingQueryParams={searchParams} /> ); @@ -232,12 +240,7 @@ function UsersChartCard({ async function WalletDistributionCard({ data }: { data: WalletStats[] }) { const formattedData = await Promise.all( data - .filter( - (w) => - w.walletType !== "smart" && - w.walletType !== "unknown" && - w.walletType !== "inApp", - ) + .filter((w) => w.walletType !== "smart" && w.walletType !== "smartWallet") .map(async (w) => { const wallet = await getWalletInfo(w.walletType as WalletId).catch( () => ({ name: w.walletType }), @@ -254,16 +257,12 @@ async function WalletDistributionCard({ data }: { data: WalletStats[] }) { return ( { - return { - value: uniqueWalletsConnected, - label: walletName, - // Multiply by 2 to avoid colors used by authentication methods - fill: `hsl(var(--chart-${(index + 1) * 2}))`, - }; - }, - )} + data={formattedData.map(({ walletName, uniqueWalletsConnected }) => { + return { + value: uniqueWalletsConnected, + label: walletName, + }; + })} /> ); } @@ -272,14 +271,10 @@ function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) { return ( ({ - value: uniqueWalletsConnected, - label: authenticationMethod, - // Multiply by 2 and add 1 to avoid colors used by wallets connected - fill: `hsl(var(--chart-${index * 2 + 1}))`, - }), - )} + data={data.map(({ authenticationMethod, uniqueWalletsConnected }) => ({ + value: uniqueWalletsConnected, + label: authenticationMethod, + }))} /> ); } @@ -305,17 +300,18 @@ async function TotalSponsoredCard({ data }: { data: UserOpStatsByChain[] }) { label: chain?.name || item.chainId || "Unknown", value: item.sponsoredUsd, icon: chain?.icon?.url ? ( - // eslint-disable-next-line @next/next/no-img-element - +
+ +
) : undefined, fill: `hsl(var(--chart-${index + 1}))`, }; @@ -345,17 +341,18 @@ async function UserOpUsageCard({ data }: { data: UserOpStatsByChain[] }) { label: chain?.name || item.chainId || "Unknown", value: item.successful + item.failed, icon: chain?.icon?.url ? ( - // eslint-disable-next-line @next/next/no-img-element - +
+ +
) : undefined, fill: `hsl(var(--chart-${index + 1}))`, }; diff --git a/apps/dashboard/src/components/analytics/range-selector.tsx b/apps/dashboard/src/components/analytics/range-selector.tsx index 018166b5f84..1840210d2bc 100644 --- a/apps/dashboard/src/components/analytics/range-selector.tsx +++ b/apps/dashboard/src/components/analytics/range-selector.tsx @@ -12,6 +12,7 @@ import type { import { IntervalSelector } from "components/analytics/interval-selector"; import { differenceInDays } from "date-fns"; import { usePathname, useSearchParams } from "next/navigation"; +import { useState } from "react"; export function RangeSelector({ range, @@ -20,11 +21,16 @@ export function RangeSelector({ const pathname = usePathname(); const searchParams = useSearchParams(); const router = useDashboardRouter(); + const [localRange, setRange] = useState( + range || getLastNDaysRange("last-120"), + ); + const [localInterval, setInterval] = useState<"day" | "week">(interval); - const { data: computedRange } = useQuery({ + useQuery({ queryKey: ["analytics-range", searchParams?.toString(), range], queryFn: async () => { if (range) { + setRange(range); return range; } if (searchParams) { @@ -35,17 +41,43 @@ export function RangeSelector({ const toStringified = searchParams.get("to"); const to = toStringified ? new Date(toStringified) : new Date(); const type = (searchParams.get("type") as DurationId) || "last-120"; + setRange({ from, to, type }); return { from, to, type } satisfies Range; } return getLastNDaysRange("last-120"); }, }); + // prefetch for each interval and default range + useQuery({ + queryKey: ["analytics-range", searchParams?.toString()], + queryFn: async () => { + const newSearchParams = new URLSearchParams(searchParams || {}); + for (const interval of ["day", "week"] as const) { + newSearchParams.set("interval", interval); + for (const type of [ + "last-120", + "last-60", + "last-30", + "last-7", + ] as const) { + const newRange = getLastNDaysRange(type); + newSearchParams.set("from", newRange.from.toISOString()); + newSearchParams.set("to", newRange.to.toISOString()); + newSearchParams.set("type", newRange.type); + router.prefetch(`${pathname}?${newSearchParams.toString()}`); + } + } + return true; + }, + }); + return (
{ + setRange(newRange); const days = differenceInDays(newRange.to, newRange.from); const interval = days > 30 ? "week" : "day"; const newSearchParams = new URLSearchParams(searchParams || {}); @@ -57,8 +89,9 @@ export function RangeSelector({ }} /> { + setInterval(newInterval); const newSearchParams = new URLSearchParams(searchParams || {}); newSearchParams.set("interval", newInterval); router.push(`${pathname}?${newSearchParams.toString()}`); diff --git a/apps/dashboard/src/components/shared/ConnectSDKCard.tsx b/apps/dashboard/src/components/shared/ConnectSDKCard.tsx deleted file mode 100644 index 49b2582ba3d..00000000000 --- a/apps/dashboard/src/components/shared/ConnectSDKCard.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon"; -import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; -import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; -import { UnityIcon } from "components/icons/brand-icons/UnityIcon"; -import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon"; -import { DocLink } from "./DocLink"; - -export function ConnectSDKCard({ - title, - description, -}: { title?: string; description?: string }) { - return ( -
-

- {title || "Connect SDK"} -

-

- {description || "Add the Connect SDK to your app."} -

- -
- - - - - - -
- - -
- ); -} - -function BackgroundPattern() { - const color = "hsl(var(--foreground)/50%)"; - return ( -
- ); -}