diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx index 2cb3232d4f9..fb4ac246844 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx @@ -54,8 +54,8 @@ export default async function TeamLayout(props: { exactMatch: true, }, { - path: `/team/${params.team_slug}/~/projects`, - name: "Projects", + path: `/team/${params.team_slug}/~/analytics`, + name: "Analytics", }, { path: `/team/${params.team_slug}/~/contracts`, diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx index 026f703378b..6d55bdbf20d 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx @@ -1,336 +1,33 @@ -import { - getInAppWalletUsage, - getUserOpUsage, - getWalletConnections, - getWalletUsers, -} from "@/api/analytics"; -import { redirect } from "next/navigation"; - -import type { - InAppWalletStats, - WalletStats, - WalletUserStats, -} from "types/analytics"; - -import { - type DurationId, - type Range, - getLastNDaysRange, -} from "components/analytics/date-range-selector"; - -import { type WalletId, getWalletInfo } from "thirdweb/wallets"; -import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader"; -import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard"; -import { EmptyState } from "../../components/Analytics/EmptyState"; -import { PieChartCard } from "../../components/Analytics/PieChartCard"; - +import { getProjects } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; -import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; -import { getValidAccount } from "app/account/settings/getAccount"; -import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard"; -import { Changelog, type ChangelogItem } from "components/dashboard/Changelog"; -import { Suspense } from "react"; -import { TotalSponsoredChartCardUI } from "./_components/TotalSponsoredCard"; - -// revalidate every 5 minutes -export const revalidate = 300; - -type SearchParams = { - usersChart?: string; - from?: string; - to?: string; - type?: string; - interval?: string; -}; +import { Changelog } from "components/dashboard/Changelog"; +import { redirect } from "next/navigation"; +import { TeamProjectsPage } from "./~/projects/TeamProjectsPage"; -export default async function TeamOverviewPage(props: { +export default async function Page(props: { params: Promise<{ team_slug: string }>; - searchParams: Promise<SearchParams>; }) { - const changelog = await getChangelog(); - const [params, searchParams] = await Promise.all([ - props.params, - props.searchParams, - ]); - - const account = await getValidAccount(`/team/${params.team_slug}`); + const params = await props.params; const team = await getTeamBySlug(params.team_slug); if (!team) { redirect("/team"); } - const interval = (searchParams.interval as "day" | "week") ?? "week"; - const rangeType = (searchParams.type as DurationId) || "last-120"; - const range: Range = { - from: new Date(searchParams.from ?? getLastNDaysRange("last-120").from), - to: new Date(searchParams.to ?? getLastNDaysRange("last-120").to), - type: rangeType, - }; + const projects = await getProjects(params.team_slug); return ( - <div className="flex grow flex-col"> - <div className="border-b"> - <AnalyticsHeader - title="Team Overview" - interval={interval} - range={range} - /> + <div className="container flex grow flex-col gap-12 py-8 lg:flex-row"> + <div className="flex grow flex-col"> + <h1 className="mb-4 font-semibold text-xl tracking-tight">Projects</h1> + <TeamProjectsPage projects={projects} team={team} /> </div> - <div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16 xl:flex-row"> - <Suspense fallback={<GenericLoadingPage />}> - <OverviewPageContent - account={account} - range={range} - interval={interval} - searchParams={searchParams} - /> - </Suspense> - <div className="shrink-0 max-md:container max-xl:hidden lg:w-[320px]"> - <h2 className="mb-4 font-semibold text-lg tracking-tight"> - Latest changes - </h2> - <Changelog changelog={changelog} /> - </div> - </div> - </div> - ); -} - -async function OverviewPageContent(props: { - account: Account; - range: Range; - interval: "day" | "week"; - searchParams: SearchParams; -}) { - const { account, range, interval, searchParams } = props; - - const [ - walletConnections, - walletUserStatsTimeSeries, - inAppWalletUsage, - userOpUsageTimeSeries, - userOpUsage, - ] = await Promise.all([ - // Aggregated wallet connections - getWalletConnections({ - accountId: account.id, - from: range.from, - to: range.to, - period: "all", - }), - // Time series data for wallet users - getWalletUsers({ - accountId: account.id, - from: range.from, - to: range.to, - period: interval, - }), - // In-app wallet usage - getInAppWalletUsage({ - accountId: account.id, - from: range.from, - to: range.to, - period: "all", - }), - // User operations usage - getUserOpUsage({ - accountId: account.id, - from: range.from, - to: range.to, - period: interval, - }), - getUserOpUsage({ - accountId: account.id, - from: range.from, - to: range.to, - period: "all", - }), - ]); - - const isEmpty = - !walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) && - walletConnections.length === 0 && - inAppWalletUsage.length === 0 && - userOpUsage.length === 0; - - if (isEmpty) { - return <EmptyState />; - } - - return ( - <div className="flex grow flex-col gap-6"> - {walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? ( - <div className=""> - <UsersChartCard - userStats={walletUserStatsTimeSeries} - searchParams={searchParams} - /> - </div> - ) : ( - <EmptyStateCard - metric="Connect" - link="https://portal.thirdweb.com/connect/quickstart" - /> - )} - <div className="grid gap-6 max-md:px-6 md:grid-cols-2"> - {walletConnections.length > 0 ? ( - <WalletDistributionCard data={walletConnections} /> - ) : ( - <EmptyStateCard - metric="Connect" - link="https://portal.thirdweb.com/connect/quickstart" - /> - )} - {inAppWalletUsage.length > 0 ? ( - <AuthMethodDistributionCard data={inAppWalletUsage} /> - ) : ( - <EmptyStateCard - metric="In-App Wallets" - link="https://portal.thirdweb.com/typescript/v5/inAppWallet" - /> - )} + <div className="shrink-0 lg:w-[320px]"> + <h2 className="mb-4 font-semibold text-xl tracking-tight"> + Latest changes + </h2> + <Changelog /> </div> - {userOpUsage.length > 0 ? ( - <TotalSponsoredChartCardUI - searchParams={searchParams} - data={userOpUsageTimeSeries} - aggregatedData={userOpUsage} - className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0" - /> - ) : ( - <EmptyStateCard - metric="Sponsored Transactions" - link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started" - /> - )} </div> ); } - -async function getChangelog() { - const res = await fetch( - "https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=5", - ); - const json = await res.json(); - return json.posts as ChangelogItem[]; -} - -type UserMetrics = { - totalUsers: number; - activeUsers: number; - newUsers: number; - returningUsers: number; -}; - -type TimeSeriesMetrics = UserMetrics & { - date: string; -}; - -function processTimeSeriesData( - userStats: WalletUserStats[], -): TimeSeriesMetrics[] { - const metrics: TimeSeriesMetrics[] = []; - - let cumulativeUsers = 0; - for (const stat of userStats) { - cumulativeUsers += stat.newUsers ?? 0; - metrics.push({ - date: stat.date, - activeUsers: stat.totalUsers ?? 0, - returningUsers: stat.returningUsers ?? 0, - newUsers: stat.newUsers ?? 0, - totalUsers: cumulativeUsers, - }); - } - - return metrics; -} - -function UsersChartCard({ - userStats, - searchParams, -}: { - userStats: WalletUserStats[]; - searchParams?: { [key: string]: string | string[] | undefined }; -}) { - const timeSeriesData = processTimeSeriesData(userStats); - - const chartConfig = { - activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" }, - totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" }, - newUsers: { label: "New Users", color: "hsl(var(--chart-3))" }, - returningUsers: { - label: "Returning Users", - color: "hsl(var(--chart-4))", - }, - } as const; - - return ( - <CombinedBarChartCard - className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0" - title="Users" - chartConfig={chartConfig} - activeChart={ - (searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers" - } - data={timeSeriesData} - aggregateFn={(_data, key) => - timeSeriesData[timeSeriesData.length - 2]?.[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 - } - queryKey="usersChart" - existingQueryParams={searchParams} - /> - ); -} - -async function WalletDistributionCard({ data }: { data: WalletStats[] }) { - const formattedData = await Promise.all( - data - .filter((w) => w.walletType !== "smart" && w.walletType !== "smartWallet") - .map(async (w) => { - const wallet = await getWalletInfo(w.walletType as WalletId).catch( - () => ({ name: w.walletType }), - ); - return { - walletType: w.walletType, - uniqueWalletsConnected: w.uniqueWalletsConnected, - totalConnections: w.totalConnections, - walletName: wallet.name, - }; - }), - ); - - return ( - <PieChartCard - title="Wallets Connected" - data={formattedData.map(({ walletName, uniqueWalletsConnected }) => { - return { - value: uniqueWalletsConnected, - label: walletName, - }; - })} - /> - ); -} - -function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) { - return ( - <PieChartCard - title="Social Authentication" - data={data.map(({ authenticationMethod, uniqueWalletsConnected }) => ({ - value: uniqueWalletsConnected, - label: authenticationMethod, - }))} - /> - ); -} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx new file mode 100644 index 00000000000..1ba33619388 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx @@ -0,0 +1,316 @@ +import { + getInAppWalletUsage, + getUserOpUsage, + getWalletConnections, + getWalletUsers, +} from "@/api/analytics"; +import { redirect } from "next/navigation"; + +import type { + InAppWalletStats, + WalletStats, + WalletUserStats, +} from "types/analytics"; + +import { + type DurationId, + type Range, + getLastNDaysRange, +} from "components/analytics/date-range-selector"; + +import { type WalletId, getWalletInfo } from "thirdweb/wallets"; +import { AnalyticsHeader } from "../../../../components/Analytics/AnalyticsHeader"; +import { CombinedBarChartCard } from "../../../../components/Analytics/CombinedBarChartCard"; +import { EmptyState } from "../../../../components/Analytics/EmptyState"; +import { PieChartCard } from "../../../../components/Analytics/PieChartCard"; + +import { getTeamBySlug } from "@/api/team"; +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { getValidAccount } from "app/account/settings/getAccount"; +import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard"; +import { Suspense } from "react"; +import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard"; + +// revalidate every 5 minutes +export const revalidate = 300; + +type SearchParams = { + usersChart?: string; + from?: string; + to?: string; + type?: string; + interval?: string; +}; + +export default async function TeamOverviewPage(props: { + params: Promise<{ team_slug: string }>; + searchParams: Promise<SearchParams>; +}) { + const [params, searchParams] = await Promise.all([ + props.params, + props.searchParams, + ]); + + const account = await getValidAccount(`/team/${params.team_slug}`); + const team = await getTeamBySlug(params.team_slug); + + if (!team) { + redirect("/team"); + } + + const interval = (searchParams.interval as "day" | "week") ?? "week"; + const rangeType = (searchParams.type as DurationId) || "last-120"; + const range: Range = { + from: new Date(searchParams.from ?? getLastNDaysRange("last-120").from), + to: new Date(searchParams.to ?? getLastNDaysRange("last-120").to), + type: rangeType, + }; + + return ( + <div className="flex grow flex-col"> + <div className="border-b"> + <AnalyticsHeader title="Analytics" interval={interval} range={range} /> + </div> + <div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16 xl:flex-row"> + <Suspense fallback={<GenericLoadingPage />}> + <OverviewPageContent + account={account} + range={range} + interval={interval} + searchParams={searchParams} + /> + </Suspense> + </div> + </div> + ); +} + +async function OverviewPageContent(props: { + account: Account; + range: Range; + interval: "day" | "week"; + searchParams: SearchParams; +}) { + const { account, range, interval, searchParams } = props; + + const [ + walletConnections, + walletUserStatsTimeSeries, + inAppWalletUsage, + userOpUsageTimeSeries, + userOpUsage, + ] = await Promise.all([ + // Aggregated wallet connections + getWalletConnections({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + // Time series data for wallet users + getWalletUsers({ + accountId: account.id, + from: range.from, + to: range.to, + period: interval, + }), + // In-app wallet usage + getInAppWalletUsage({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + // User operations usage + getUserOpUsage({ + accountId: account.id, + from: range.from, + to: range.to, + period: interval, + }), + getUserOpUsage({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + ]); + + const isEmpty = + !walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) && + walletConnections.length === 0 && + inAppWalletUsage.length === 0 && + userOpUsage.length === 0; + + if (isEmpty) { + return <EmptyState />; + } + + return ( + <div className="flex grow flex-col gap-6"> + {walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? ( + <div className=""> + <UsersChartCard + userStats={walletUserStatsTimeSeries} + searchParams={searchParams} + /> + </div> + ) : ( + <EmptyStateCard + metric="Connect" + link="https://portal.thirdweb.com/connect/quickstart" + /> + )} + <div className="grid gap-6 max-md:px-6 md:grid-cols-2"> + {walletConnections.length > 0 ? ( + <WalletDistributionCard data={walletConnections} /> + ) : ( + <EmptyStateCard + metric="Connect" + link="https://portal.thirdweb.com/connect/quickstart" + /> + )} + {inAppWalletUsage.length > 0 ? ( + <AuthMethodDistributionCard data={inAppWalletUsage} /> + ) : ( + <EmptyStateCard + metric="In-App Wallets" + link="https://portal.thirdweb.com/typescript/v5/inAppWallet" + /> + )} + </div> + {userOpUsage.length > 0 ? ( + <TotalSponsoredChartCardUI + searchParams={searchParams} + data={userOpUsageTimeSeries} + aggregatedData={userOpUsage} + className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0" + /> + ) : ( + <EmptyStateCard + metric="Sponsored Transactions" + link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started" + /> + )} + </div> + ); +} + +type UserMetrics = { + totalUsers: number; + activeUsers: number; + newUsers: number; + returningUsers: number; +}; + +type TimeSeriesMetrics = UserMetrics & { + date: string; +}; + +function processTimeSeriesData( + userStats: WalletUserStats[], +): TimeSeriesMetrics[] { + const metrics: TimeSeriesMetrics[] = []; + + let cumulativeUsers = 0; + for (const stat of userStats) { + cumulativeUsers += stat.newUsers ?? 0; + metrics.push({ + date: stat.date, + activeUsers: stat.totalUsers ?? 0, + returningUsers: stat.returningUsers ?? 0, + newUsers: stat.newUsers ?? 0, + totalUsers: cumulativeUsers, + }); + } + + return metrics; +} + +function UsersChartCard({ + userStats, + searchParams, +}: { + userStats: WalletUserStats[]; + searchParams?: { [key: string]: string | string[] | undefined }; +}) { + const timeSeriesData = processTimeSeriesData(userStats); + + const chartConfig = { + activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" }, + totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" }, + newUsers: { label: "New Users", color: "hsl(var(--chart-3))" }, + returningUsers: { + label: "Returning Users", + color: "hsl(var(--chart-4))", + }, + } as const; + + return ( + <CombinedBarChartCard + className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0" + title="Users" + chartConfig={chartConfig} + activeChart={ + (searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers" + } + data={timeSeriesData} + aggregateFn={(_data, key) => + timeSeriesData[timeSeriesData.length - 2]?.[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 + } + queryKey="usersChart" + existingQueryParams={searchParams} + /> + ); +} + +async function WalletDistributionCard({ data }: { data: WalletStats[] }) { + const formattedData = await Promise.all( + data + .filter((w) => w.walletType !== "smart" && w.walletType !== "smartWallet") + .map(async (w) => { + const wallet = await getWalletInfo(w.walletType as WalletId).catch( + () => ({ name: w.walletType }), + ); + return { + walletType: w.walletType, + uniqueWalletsConnected: w.uniqueWalletsConnected, + totalConnections: w.totalConnections, + walletName: wallet.name, + }; + }), + ); + + return ( + <PieChartCard + title="Wallets Connected" + data={formattedData.map(({ walletName, uniqueWalletsConnected }) => { + return { + value: uniqueWalletsConnected, + label: walletName, + }; + })} + /> + ); +} + +function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) { + return ( + <PieChartCard + title="Social Authentication" + data={data.map(({ authenticationMethod, uniqueWalletsConnected }) => ({ + value: uniqueWalletsConnected, + label: authenticationMethod, + }))} + /> + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx index 79a538cd8d1..b7ee115f177 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/select"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog"; -import { ChevronDownIcon, SearchIcon } from "lucide-react"; +import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; @@ -58,9 +58,7 @@ export function TeamProjectsPage(props: { } return ( - <div className="container "> - <div className="h-10" /> - + <div className="flex grow flex-col"> {/* Filters + Add New */} <div className="flex flex-col gap-4 md:flex-row md:items-center"> <SearchInput value={searchTerm} onValueChange={setSearchTerm} /> @@ -77,11 +75,21 @@ export function TeamProjectsPage(props: { {/* Projects */} {projectsToShow.length === 0 ? ( - <div className="flex h-[450px] items-center justify-center rounded-lg border border-border "> - No projects found + <div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border"> + <div className="flex flex-col items-center"> + <p className="mb-5 text-center">No projects created</p> + <Button + className="gap-2" + onClick={() => setIsCreateProjectDialogOpen(true)} + variant="outline" + > + <PlusIcon className="size-4" /> + Create a Project + </Button> + </div> </div> ) : ( - <div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3"> + <div className="grid grid-cols-1 gap-5 md:grid-cols-2"> {projectsToShow.map((project) => { return ( <ProjectCard @@ -94,8 +102,6 @@ export function TeamProjectsPage(props: { </div> )} - <div className="h-10" /> - <LazyCreateAPIKeyDialog open={isCreateProjectDialogOpen} onOpenChange={setIsCreateProjectDialogOpen} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/page.tsx deleted file mode 100644 index d160e4cc8ca..00000000000 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { getProjects } from "@/api/projects"; -import { getTeamBySlug } from "@/api/team"; -import { redirect } from "next/navigation"; -import { TeamProjectsPage } from "./TeamProjectsPage"; - -export default async function Page(props: { - params: Promise<{ team_slug: string }>; -}) { - const params = await props.params; - const team = await getTeamBySlug(params.team_slug); - - if (!team) { - redirect("/team"); - } - - const projects = await getProjects(params.team_slug); - - return <TeamProjectsPage projects={projects} team={team} />; -} diff --git a/apps/dashboard/src/components/dashboard/Changelog.tsx b/apps/dashboard/src/components/dashboard/Changelog.tsx index c618c59239f..53da85247af 100644 --- a/apps/dashboard/src/components/dashboard/Changelog.tsx +++ b/apps/dashboard/src/components/dashboard/Changelog.tsx @@ -1,20 +1,16 @@ -import { Skeleton } from "@/components/ui/skeleton"; -import { formatDistance } from "date-fns/formatDistance"; +import { formatDistance } from "date-fns"; import { ArrowRightIcon } from "lucide-react"; import Link from "next/link"; -import { ClientOnly } from "../ClientOnly/ClientOnly"; -export interface ChangelogItem { +type ChangelogItem = { published_at: string; title: string; url: string; -} +}; -interface ChangelogProps { - changelog: ChangelogItem[]; -} +export async function Changelog() { + const changelog = await getChangelog(); -export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => { return ( <div className="relative flex flex-col gap-6 border-border border-l py-2"> {changelog.map((item) => ( @@ -31,11 +27,9 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => { {item.title} </Link> <div className="mt-1 text-muted-foreground text-xs opacity-70"> - <ClientOnly ssr={<Skeleton className="h-2 w-28" />}> - {formatDistance(new Date(item.published_at), Date.now(), { - addSuffix: true, - })} - </ClientOnly> + {formatDistance(new Date(item.published_at), Date.now(), { + addSuffix: true, + })} </div> </div> </div> @@ -49,4 +43,12 @@ export const Changelog: React.FC<ChangelogProps> = ({ changelog }) => { </Link> </div> ); -}; +} + +async function getChangelog() { + const res = await fetch( + "https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=10", + ); + const json = await res.json(); + return json.posts as ChangelogItem[]; +} diff --git a/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx b/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx index cb113e59227..5833967e979 100644 --- a/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx +++ b/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx @@ -753,7 +753,7 @@ export const CodeOverview: React.FC<CodeOverviewProps> = ({ thirdweb's infrastructure services. If you don't have any API keys yet you can create one by creating a project for free from the{" "} - <Link href="/team/~/~/projects" color="primary.500"> + <Link href="/team" color="primary.500"> dashboard </Link> . diff --git a/apps/portal/src/app/account/api-keys/access/page.mdx b/apps/portal/src/app/account/api-keys/access/page.mdx index f80c63d05d4..8bed4b23eab 100644 --- a/apps/portal/src/app/account/api-keys/access/page.mdx +++ b/apps/portal/src/app/account/api-keys/access/page.mdx @@ -26,7 +26,7 @@ To modify the allowed Domain Id or Bundle Ids: <Step title="Go to Dashboard"> -Go to **[thirdweb dashboard](https://thirdweb.com/team/~/~/projects)** +Go to **[thirdweb dashboard](https://thirdweb.com/team)** </Step> diff --git a/apps/portal/src/app/account/api-keys/create/page.mdx b/apps/portal/src/app/account/api-keys/create/page.mdx index 95545588ee7..b6c290a187a 100644 --- a/apps/portal/src/app/account/api-keys/create/page.mdx +++ b/apps/portal/src/app/account/api-keys/create/page.mdx @@ -17,7 +17,7 @@ export const metadata = createMetadata({ <Step title="Login to dashboard"> -Log in to the dashboard and go to your **[Team projects page](https://thirdweb.com/team/~/~/projects)** +Log in to the dashboard and go to your **[Team projects page](https://thirdweb.com/team)** </Step> diff --git a/apps/portal/src/app/account/api-keys/delete/page.mdx b/apps/portal/src/app/account/api-keys/delete/page.mdx index 0c785f55a24..dfa811f8179 100644 --- a/apps/portal/src/app/account/api-keys/delete/page.mdx +++ b/apps/portal/src/app/account/api-keys/delete/page.mdx @@ -20,7 +20,7 @@ Deleting an API key will invalidate it, making it no longer usable. <Steps> <Step title="Go to Dashboard"> - Go to **[your team's projects page](https://thirdweb.com/team/~/~/projects/)** on the dashboard. + Go to **[your team's projects page](https://thirdweb.com/team)** on the dashboard. </Step> <Step title='Choose API Key'> diff --git a/apps/portal/src/app/account/api-keys/edit-services/page.mdx b/apps/portal/src/app/account/api-keys/edit-services/page.mdx index 29e0b0dcce6..dc28d42acee 100644 --- a/apps/portal/src/app/account/api-keys/edit-services/page.mdx +++ b/apps/portal/src/app/account/api-keys/edit-services/page.mdx @@ -23,7 +23,7 @@ All services on API keys are enabled by default. If you want to disable any serv <Step title="Go to Dashboard"> -Go to **[your team's projects page](https://thirdweb.com/team/~/~/projects)** on the dashboard. +Go to **[your team's projects page](https://thirdweb.com/team)** on the dashboard. </Step> diff --git a/apps/portal/src/app/connect/pay/fee-sharing/page.mdx b/apps/portal/src/app/connect/pay/fee-sharing/page.mdx index 895d963e694..25633d98867 100644 --- a/apps/portal/src/app/connect/pay/fee-sharing/page.mdx +++ b/apps/portal/src/app/connect/pay/fee-sharing/page.mdx @@ -18,6 +18,6 @@ For example, if a user purchases $100 worth of Polygon through your application, Plase note that fee sharing only applies to fees collected through swaps and bridges. Fiat purchases do not currently qualify for fee sharing. -To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Pay in [thirdweb dashboard](https://www.thirdweb.com/team/~/~/projects). +To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Pay in [thirdweb dashboard](https://www.thirdweb.com/team). <DocImage src={paySettingsImage} /> diff --git a/apps/portal/src/app/connect/pay/guides/build-a-custom-experience/page.mdx b/apps/portal/src/app/connect/pay/guides/build-a-custom-experience/page.mdx index 497bc76f64c..184461922bf 100644 --- a/apps/portal/src/app/connect/pay/guides/build-a-custom-experience/page.mdx +++ b/apps/portal/src/app/connect/pay/guides/build-a-custom-experience/page.mdx @@ -41,7 +41,7 @@ In this guide, we'll show you how to purchase 0.01 Base ETH from USD in Typescri /> <Step title='Get Your Client ID'> -Log in to the [thirdweb dashboard](https://thirdweb.com/team/~/~/projects). Click on Create New > Project to get your **Client ID**. You'll need your Client ID to interact with the Connect SDK. +Log in to the [thirdweb dashboard](https://thirdweb.com/team). Click on Create New > Project to get your **Client ID**. You'll need your Client ID to interact with the Connect SDK. </Step> </Step> diff --git a/apps/portal/src/app/connect/quickstart/page.mdx b/apps/portal/src/app/connect/quickstart/page.mdx index 774477e7ca4..40d4efd165b 100644 --- a/apps/portal/src/app/connect/quickstart/page.mdx +++ b/apps/portal/src/app/connect/quickstart/page.mdx @@ -17,7 +17,7 @@ npm i thirdweb <Step title="Get Your Client ID"> -Log in to the [thirdweb dashboard](https://thirdweb.com/team/~/~/projects). Create a new project to get your **Client ID**. +Log in to the [thirdweb dashboard](https://thirdweb.com/team). Create a new project to get your **Client ID**. </Step> diff --git a/apps/portal/src/app/contracts/modular-contracts/get-started/deploy-modular-contract/page.mdx b/apps/portal/src/app/contracts/modular-contracts/get-started/deploy-modular-contract/page.mdx index bf48df08d1d..28a391bb75c 100644 --- a/apps/portal/src/app/contracts/modular-contracts/get-started/deploy-modular-contract/page.mdx +++ b/apps/portal/src/app/contracts/modular-contracts/get-started/deploy-modular-contract/page.mdx @@ -34,7 +34,7 @@ as this guide will build on top of those previous guides. <Steps> <Step title="Grab an API Key from the thirdweb Dashboard"> -Go to the [thirdweb Dashboard](https://thirdweb.com/team/~/~/projects), Create a new Project to obtain API Key +Go to the [thirdweb Dashboard](https://thirdweb.com/team), Create a new Project to obtain API Key <DocImage src={ createApiKey } /> diff --git a/apps/portal/src/app/infrastructure/rpc-edge/overview/page.mdx b/apps/portal/src/app/infrastructure/rpc-edge/overview/page.mdx index d816ee4a568..0986befbbe4 100644 --- a/apps/portal/src/app/infrastructure/rpc-edge/overview/page.mdx +++ b/apps/portal/src/app/infrastructure/rpc-edge/overview/page.mdx @@ -21,7 +21,7 @@ Remote Procedure Call (RPC) Edge provides reliable access to querying data and i By default, we provide publicly available RPCs for over 900+ EVM Networks. [View the default RPC endpoints](https://thirdweb.com/chainlist) for each supported chain. <Callout variant="info" title="Accessing your RPC URL"> - To use RPC Edge with other tools like Anvil, take a chain's [public RPC endpoint](https://thirdweb.com/chainlist) and attach your app's client ID from Project > Settings page in [dashboard](https://thirdweb.com/team/~/~/projects) `https://<chainId>.rpc.thirdweb.com/<clientId>`. + To use RPC Edge with other tools like Anvil, take a chain's [public RPC endpoint](https://thirdweb.com/chainlist) and attach your app's client ID from Project > Settings page in [dashboard](https://thirdweb.com/team) `https://<chainId>.rpc.thirdweb.com/<clientId>`. </Callout> #### Features diff --git a/apps/portal/src/app/payments/nft-checkout/api-reference/page.mdx b/apps/portal/src/app/payments/nft-checkout/api-reference/page.mdx index a04ffaa0886..4b1e7ebf000 100644 --- a/apps/portal/src/app/payments/nft-checkout/api-reference/page.mdx +++ b/apps/portal/src/app/payments/nft-checkout/api-reference/page.mdx @@ -12,7 +12,7 @@ The Payments API enables all the functionalities needed to create checkouts. [View the full API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc) You must use an API Secret Key to make authenticated calls. These calls should be -made from your backend. [Create an API Key by creating a project from your thirdweb dashboard.](https://thirdweb.com/team/~/~/projects) +made from your backend. [Create an API Key by creating a project from your thirdweb dashboard.](https://thirdweb.com/team) Provide your secret key as a header: `x-secret-key: your_api_secret_key` diff --git a/apps/portal/src/app/react-native/v0/getting-started/page.mdx b/apps/portal/src/app/react-native/v0/getting-started/page.mdx index 4068ab8d95b..ce2c1052585 100644 --- a/apps/portal/src/app/react-native/v0/getting-started/page.mdx +++ b/apps/portal/src/app/react-native/v0/getting-started/page.mdx @@ -18,7 +18,7 @@ Our SDK uses a [Provider Pattern](https://flexiple.com/react/provider-pattern-wi You will require an API key to use thirdweb's infrastructure services with the SDK. -you need to first obtain an API key from the [dashboard](https://thirdweb.com/team/~/~/projects) by creating a new project and then copy the "Client ID" and pass it to the `ThirdwebPovider` as the `clientId` prop. +you need to first obtain an API key from the [dashboard](https://thirdweb.com/team) by creating a new project and then copy the "Client ID" and pass it to the `ThirdwebPovider` as the `clientId` prop. </Callout> diff --git a/apps/portal/src/app/react/v4/ThirdwebProvider/page.mdx b/apps/portal/src/app/react/v4/ThirdwebProvider/page.mdx index dfe93c6f24b..1807d065f1e 100644 --- a/apps/portal/src/app/react/v4/ThirdwebProvider/page.mdx +++ b/apps/portal/src/app/react/v4/ThirdwebProvider/page.mdx @@ -26,7 +26,7 @@ The `ThirdwebProvider` is a wrapper component that provides access to all of the You will require an API key to use thirdweb's infrastructure services with the SDK. -you need to first obtain an API key from the [dashboard](https://thirdweb.com/team/~/~/projects) by creating a new project and then copy the "Client ID" and pass it to the `ThirdwebProvider` as the `clientId` prop. +you need to first obtain an API key from the [dashboard](https://thirdweb.com/team) by creating a new project and then copy the "Client ID" and pass it to the `ThirdwebProvider` as the `clientId` prop. </Callout> diff --git a/apps/portal/src/app/react/v5/rainbow-kit-migrate/page.mdx b/apps/portal/src/app/react/v5/rainbow-kit-migrate/page.mdx index 07427244585..720652c0d66 100644 --- a/apps/portal/src/app/react/v5/rainbow-kit-migrate/page.mdx +++ b/apps/portal/src/app/react/v5/rainbow-kit-migrate/page.mdx @@ -85,7 +85,7 @@ Learn how to migrate to thirdweb's ConnectButton component from [RainbowKit](htt ); }; ``` - <Callout variant="info" title="Client ID">Get a free client ID to use in your application by creating a project in [thirdweb's dashboard](https://thirdweb.com/team/~/~/projects).</Callout> + <Callout variant="info" title="Client ID">Get a free client ID to use in your application by creating a project in [thirdweb's dashboard](https://thirdweb.com/team).</Callout> </Step> <Step title="Customize"> To customize your ConnectButton component, view the cheatsheet below or [view the customization documentation](https://portal.thirdweb.com/connect/sign-in/customization).