Replace legacy dashboard with Teams and Projects dashboard (#5327)

## Changes Summary

### Huge changes
* Delete all `/dashboard/**` pages and make the `/team` the default
* Rename all the links `/dashboard/**` to the new equivalent + Setup redirects

### New pages Added in team and account layouts
* `/account/contracts`
* `/team/<team-slug>/~/contracts`
* `/account/devices`
* new team overview page
* `/team/<team-slug>/~/usage/storage`

### Pages Moved
* Engine pages moved from project to team
* Ecosystem pages moved from project to team
* Team's projects page moved to /projects ( `/team/<team-slug>/~/projects` )

### Links to these pages removed for now (page not deleted) in new dashboard because they are not yet ready
* `/account/wallets`, `/account/settings`

### Minor changes
* Add the `AutoConnect` component directly where we render `ThirdwebProvider` so it is always triggered and not only when certain components are rendered, and remove AutoConnect at component level
* Move `/chain/validate` to app router inside (dashboard) layout
* Remove unnecessary breadcrumb on /explore base page
* Various UI adjustments / improvements
* Add billing alerts on top of all team, project, account pages

### Fixes
* Fix theme switcher not working sometimes in app router
* Engine pages crashing in team dashboard
* Revert color changes done in a recent a PR, fix colors in project header, app footer done.
* Fix Team avatar not resolving IPFS
* Fix various react warnings
* Fix missing Overview tab on the ecosystem page

### Improvements
* Reduced redirects on in-app-wallets and connect analytics page by making the landing page the default. For example: render analytics on `/connect/in-app-wallets` instead of landing on `/connect/in-app-wallets` and then redirecting to `/connect/in-app-wallets/analytics` and then rendering the page )
This commit is contained in:
MananTank
2024-11-07 20:26:29 +00:00
parent 9f9410a77a
commit aa46e27d7c
473 changed files with 2550 additions and 5106 deletions
+69 -124
View File
@@ -1,5 +1,69 @@
const legacyDashboardToTeamRedirects = [
{
source: "/dashboard",
destination: "/team",
permanent: false,
},
{
source: "/dashboard/contracts/:path*",
destination: "/team/~/~/contracts",
permanent: false,
},
{
source: "/dashboard/connect/ecosystem/:path*",
destination: "/team/~/~/ecosystem/:path*",
permanent: false,
},
{
source: "/dashboard/engine/:path*",
destination: "/team/~/~/engine/:path*",
permanent: false,
},
{
source: "/dashboard/settings/api-keys",
destination: "/team/~/~/projects",
permanent: false,
},
{
source: "/dashboard/settings/devices",
destination: "/account/devices",
permanent: false,
},
{
source: "/dashboard/settings/billing",
destination: "/team/~/~/settings/billing",
permanent: false,
},
{
source: "/dashboard/settings/gas-credits",
destination: "/team/~/~/settings/credits",
permanent: false,
},
{
source: "/dashboard/settings/usage",
destination: "/team/~/~/usage",
permanent: false,
},
{
source: "/dashboard/settings/storage",
destination: "/team/~/~/usage/storage",
permanent: false,
},
{
source: "/dashboard/settings/notifications",
destination: "/team/~/~/settings/notifications",
permanent: false,
},
// rest of the /dashboard/* routes
{
source: "/dashboard/:path*",
destination: "/team",
permanent: false,
},
];
/** @type {import('next').NextConfig['redirects']} */
function redirects() {
async function redirects() {
return [
{
source: "/portal/:match*",
@@ -11,16 +75,6 @@ function redirects() {
destination: "/solutions/chains",
permanent: true,
},
{
source: "/dashboard/publish/:path*",
destination: "/contracts/publish/:path*",
permanent: false,
},
{
source: "/dashboard/mumbai/publish/:path*",
destination: "/contracts/publish/:path*",
permanent: false,
},
{
source: "/contracts/release",
destination: "/contracts/publish",
@@ -97,23 +151,8 @@ function redirects() {
destination: "/polygon-zkevm",
permanent: false,
},
{
source: "/settings",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
// backwards compat: page moved to pages/settings/api-key
{
source: "/dashboard/api-keys",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
// backwards compat: page moved to pages/settings/devices
{
source: "/dashboard/settings/account",
destination: "/dashboard/settings/devices",
permanent: false,
},
{
source: "/template/nft-drop",
destination: "/template/erc721",
@@ -121,17 +160,12 @@ function redirects() {
},
{
source: "/create-api-key",
destination: "/dashboard/settings/api-keys",
destination: "/team/~/~/projects",
permanent: false,
},
{
source: "/dashboard/settings",
destination: "/dashboard/settings/api-keys",
permanent: false,
},
{
source: "/dashboard/connect",
destination: "/dashboard/connect/analytics",
destination: "/team/~/~/projects",
permanent: false,
},
{
@@ -139,61 +173,6 @@ function redirects() {
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/wallet",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/rpc",
destination: "/dashboard/infrastructure/rpc-edge",
permanent: false,
},
{
source: "/dashboard/storage",
destination: "/dashboard/infrastructure/storage",
permanent: false,
},
{
source: "/smart-wallet",
destination: "/account-abstraction",
permanent: false,
},
{
source: "/dashboard/published",
destination: "/dashboard/publish",
permanent: false,
},
{
source: "/dashboard/wallets",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/wallets/analytics",
destination: "/dashboard/connect/analytics",
permanent: false,
},
{
source: "/dashboard/wallets/embedded",
destination: "/dashboard/connect/embedded-wallets",
permanent: false,
},
{
source: "/dashboard/wallets/smart-wallet",
destination: "/dashboard/connect/account-abstraction",
permanent: false,
},
{
source: "/dashboard/wallets/connect",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
},
{
source: "/dashboard/infrastructure",
destination: "/dashboard/settings/storage",
permanent: false,
},
{
source: "/dashboard/infrastructure/storage",
destination: "/dashboard/settings/storage",
@@ -204,26 +183,6 @@ function redirects() {
destination: "/chainlist",
permanent: false,
},
{
source: "/dashboard/contracts",
destination: "/dashboard/contracts/deploy",
permanent: false,
},
{
source: "/solidity-sdk",
destination: "/build",
permanent: false,
},
{
source: "/connect-wallet",
destination: "/connect",
permanent: false,
},
{
source: "/wallet-sdk",
destination: "https://portal.thirdweb.com/wallet-sdk/latest",
permanent: false,
},
{
source: "/solutions/commerce",
destination: "/solutions/loyalty",
@@ -244,27 +203,12 @@ function redirects() {
destination: "/optimism",
permanent: false,
},
{
source: "/dashboard/payments/settings",
destination: "/dashboard/connect/pay",
permanent: false,
},
{
source: "/dashboard/payments/contracts",
destination: "/dashboard/connect/pay",
permanent: false,
},
// Redirecting as ambassadors lives in community now
{
source: "/ambassadors",
destination: "/community/ambassadors",
permanent: false,
},
{
source: "/dashboard/connect/embedded-wallets",
destination: "/dashboard/connect/in-app-wallets",
permanent: false,
},
{
source: "/embedded-wallets",
destination: "/in-app-wallets",
@@ -321,6 +265,7 @@ function redirects() {
destination: "/account-abstraction",
permanent: false,
},
...legacyDashboardToTeamRedirects,
];
}
+2 -7
View File
@@ -13,13 +13,8 @@ export type Team = {
bannedAt?: string;
image?: string;
billingPlan: "pro" | "growth" | "free";
billingStatus: "validPayment" | (string & {}); // what's the other value?
billingEmail: string;
// billingExternalId: string;
// billingType: "STRIPE" | ??
// billingCustomerPayload: ?? | null
// paymentAttemptCount: number
// trialPeriodEndedAt: string | null;
billingStatus: "validPayment" | (string & {}) | null;
billingEmail: string | null;
};
export async function getTeamBySlug(slug: string) {
@@ -1,15 +0,0 @@
"use client";
import { useEffect } from "react";
import { setCookie } from "../../stores/SyncStoreToCookies";
export function DashboardTypeCookieSetter(props: {
type: "team" | "old";
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
setCookie("x-dashboard-type", props.type);
}, [props.type]);
return null;
}
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
import { getThirdwebClient } from "../../../constants/thirdweb.server";
import { Button } from "../../ui/button";
import { GradientAvatar } from "./GradientAvatar";
@@ -17,29 +18,74 @@ export const Desktop: Story = {
args: {},
};
const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar id={undefined} src={undefined} className="size-6" />
<GradientAvatar
id={undefined}
src={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="No Src, id=foo - Skeleton">
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
<GradientAvatar
id={"foo"}
src={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
<GradientAvatar id={"foo"} src={""} className="size-6" />
<BadgeContainer label="Invalid Src, id=foo - Gradient">
<GradientAvatar
id={"foo"}
src={""}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
<GradientAvatar id={"bar"} src={""} className="size-6" />
<BadgeContainer label="Invalid Src, id=bar - Gradient">
<GradientAvatar
id={"bar"}
src={""}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
<GradientAvatar
src="invalid-src"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Valid Src, https">
<GradientAvatar
src="https://picsum.photos/200/300"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="Valid Src, ipfs">
<GradientAvatar
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
id={undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
<ToggleTest />
@@ -74,7 +120,12 @@ function ToggleTest() {
<p> Src+ID is: {data ? "set" : "not set"} </p>
<BadgeContainer label="Valid Src">
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
<GradientAvatar
src={data?.src}
id={data?.id}
className="size-6"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="invalid Src">
@@ -82,6 +133,7 @@ function ToggleTest() {
className="size-6"
src={data ? "invalid-src" : undefined}
id={undefined}
client={client}
/>
</BadgeContainer>
</div>
@@ -1,5 +1,7 @@
import { Img } from "@/components/blocks/Img";
import { useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler";
import { cn } from "../../../lib/utils";
const gradients = [
@@ -32,6 +34,7 @@ export function GradientAvatar(props: {
src: string | undefined;
id: string | undefined;
className: string;
client: ThirdwebClient;
}) {
const gradient = useMemo(() => {
if (!props.id) {
@@ -40,9 +43,16 @@ export function GradientAvatar(props: {
return getGradientForString(props.id);
}, [props.id]);
const resolvedSrc = props.src
? resolveSchemeWithErrorHandler({
client: props.client,
uri: props.src,
})
: props.src;
return (
<Img
src={props.src}
src={resolvedSrc}
className={cn("rounded-full", props.className)}
fallback={
gradient ? (
@@ -51,7 +51,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
{props.links.map((link, i) => {
if ("group" in link) {
return (
<div className={cn({ "mt-6": i !== 0 })}>
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
<div className={cn({ "mt-6": i !== 0 })} key={i}>
<p className={cn("px-3 py-2 font-medium text-foreground")}>
{link.group}
</p>
@@ -63,7 +64,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
const isExternal = link.href.startsWith("http");
return (
<NavLink
key={link.href}
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
key={i}
href={link.href}
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
activeClassName="text-foreground"
@@ -19,7 +19,7 @@ export function AppFooter(props: AppFooterProps) {
return (
<footer
className={cn(
"w-full border-border border-t bg-white py-6 md:py-8 dark:bg-muted/50",
"w-full border-border border-t bg-muted/50 py-6 md:py-8",
props.className,
)}
>
@@ -50,7 +50,7 @@ export function DatePickerWithRange(props: {
id="date"
variant="outline"
className={cn(
"justify-start gap-2 bg-card text-left font-normal",
"justify-start gap-2 text-left font-normal",
props.className,
)}
>
+34 -31
View File
@@ -35,40 +35,43 @@ const DialogContent = React.forwardRef<
dialogOverlayClassName?: string;
dialogCloseClassName?: string;
}
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay className={props.dialogOverlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
>(({ className, children, ...props }, ref) => {
const { dialogOverlayClassName, dialogCloseClassName, ...restProps } = props;
return (
<DialogPortal>
<DialogOverlay className={dialogOverlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
props.dialogCloseClassName,
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...restProps}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
{children}
<DialogPrimitive.Close
className={cn(
"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
dialogCloseClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
@@ -197,6 +197,7 @@ export function TabPathLinks(props: {
isActive: l.exactMatch
? pathname === l.path
: pathname.startsWith(l.path),
isDisabled: l.isDisabled,
}))}
/>
);
+2 -2
View File
@@ -2,8 +2,8 @@ export function isLoginRequired(pathname: string) {
// remove '/' in front and then split by '/'
const paths = pathname.slice(1).split("/");
// /dashboard, /team, /cli
if (paths[0] === "dashboard" || paths[0] === "team" || paths[0] === "cli") {
// /team, /cli
if (paths[0] === "team" || paths[0] === "cli") {
return true;
}
+2 -2
View File
@@ -6,7 +6,7 @@
:root,
[data-theme="light"] {
/* bg - neutral */
--background: 0 0% 98%;
--background: 0 0% 100%;
--popover: 0 0% 100%;
--card: 0 0% 100%;
--secondary: 0 0% 92%;
@@ -46,7 +46,7 @@
[data-theme="dark"] {
/* bg - neutral */
--background: 0 0% 0%;
--card: 0 0% 5%;
--card: 0 0% 0%;
--popover: 0 0% 0%;
--secondary: 0 0% 10%;
--muted: 0 0% 9%;
@@ -15,7 +15,6 @@ import { usePathname } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import type { Chain } from "thirdweb";
import {
AutoConnect,
ConnectButton,
type NetworkSelectorProps,
useConnectModal,
@@ -126,8 +125,6 @@ export const CustomConnectWallet = (props: {
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
<Spinner className="size-4" />
</div>
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
}
@@ -147,8 +144,6 @@ export const CustomConnectWallet = (props: {
Sign In
</Link>
</Button>
{/* need auto connect here so that we actually connect */}
<AutoConnect client={thirdwebClient} />
</>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import { THIRDWEB_API_HOST } from "constants/urls";
import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
@@ -1,6 +1,4 @@
"use client";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
@@ -215,11 +213,7 @@ export function FaucetButton({
Verify your Email
</Button>
{/* We will show the modal only if the user click on it, because this is a public page */}
{showOnboarding && (
<ChakraProviderSetup>
<Onboarding onOpenChange={setShowOnBoarding} />
</ChakraProviderSetup>
)}
{showOnboarding && <Onboarding onOpenChange={setShowOnBoarding} />}
</>
);
}
@@ -58,7 +58,7 @@ export function ChainHeader(props: ChainHeaderProps) {
}
/>
<Button variant="default" asChild>
<Link href="https://thirdweb.com/dashboard" target="_blank">
<Link href="/team" target="_blank">
Get started with thirdweb
</Link>
</Button>
@@ -205,7 +205,7 @@ export default async function ChainPageLayout(props: {
}
/>
<Button variant="primary">
<Link href="https://thirdweb.com/dashboard" target="_blank">
<Link href="/team" target="_blank">
Get started
</Link>
</Button>
@@ -304,7 +304,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
return (
<Flex gap={8} direction="column">
<LazyCreateAPIKeyDialog
wording="api-key"
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
@@ -109,7 +109,7 @@ const OP_CTA = {
description:
"Successful applicants receive gas grants for use across all supported Optimism Superchain networks. These grants can sponsor gas fees for any onchain activity using our Account Abstraction tools.",
buttonText: "Apply now",
buttonLink: "/dashboard/settings/gas-credits",
buttonLink: "/team/~/~/settings/credits",
} satisfies ChainCTAProps;
const chainMetaRecord = {
@@ -1,3 +1,5 @@
"use client";
import { InlineCode } from "@/components/ui/inline-code";
import { TableContainer } from "@/components/ui/table";
import {
@@ -15,7 +17,6 @@ import { useTrack } from "hooks/analytics/useTrack";
import { useRpcValidation } from "hooks/chains/useRpcValidation";
import { AlertCircleIcon, CheckCircleIcon } from "lucide-react";
import { type ChangeEvent, useState } from "react";
import { Button, Card, FormLabel, Link } from "tw-components";
const StatusCheck = (props: {
@@ -0,0 +1,41 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { Metadata } from "next";
import Link from "next/link";
import ChainValidation from "./ChainValidation";
const title = "Validate Chain";
const description = "Validate a given chain is compatible with thirdweb.";
export const metadata: Metadata = {
title,
description,
robots: "noindex, nofollow",
};
export default function Page() {
return (
<ChakraProviderSetup>
<div className="container py-6">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-2xl tracking-tight">
Validate Chain
</h1>
<p>
Validate a given chain is compatible with{" "}
<Link
href="/chainlist"
className="text-link-foreground hover:text-foreground"
>
thirdweb
</Link>
.
</p>
</div>
<ChainValidation />
</div>
</div>
</ChakraProviderSetup>
);
}
@@ -1,38 +0,0 @@
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
export function ConnectSidebarLayout(props: {
children: React.ReactNode;
}) {
return (
<SidebarLayout
sidebarLinks={[
{
label: "Analytics",
href: "/dashboard/connect/analytics",
},
{
label: "In-App Wallets",
href: "/dashboard/connect/in-app-wallets",
},
{
label: "Ecosystem Wallets",
href: "/dashboard/connect/ecosystem",
},
{
label: "Account Abstraction",
href: "/dashboard/connect/account-abstraction",
},
{
label: "Pay",
href: "/dashboard/connect/pay",
},
{
label: "Playground",
href: "https://playground.thirdweb.com/connect/sign-in/button",
},
]}
>
{props.children}
</SidebarLayout>
);
}
@@ -1,21 +0,0 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { ApiKeysMenu } from "../../../../../components/settings/ApiKeys/Menu";
export function AccountAbstractionAPIKeysMenu(props: {
apiKeys: Pick<ApiKey, "name" | "key">[];
selectedAPIKey: Pick<ApiKey, "name" | "key">;
}) {
const router = useDashboardRouter();
return (
<ApiKeysMenu
apiKeys={props.apiKeys}
selectedKey={props.selectedAPIKey}
onSelect={(key) => {
router.push(`/dashboard/connect/account-abstraction/${key.key}`);
}}
/>
);
}
@@ -1,27 +0,0 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { useActiveWalletChain } from "thirdweb/react";
import { isOpChainId } from "../../../../team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain";
export function OpChainAlert() {
const chain = useActiveWalletChain();
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
if (!isOpChain) {
return null;
}
return (
<Alert variant="info">
<CircleAlertIcon className="size-4" />
<AlertTitle>Using the gas credits for OP chain paymaster</AlertTitle>
<AlertDescription>
Credits will automatically be applied to cover gas fees for any onchain
activity across thirdweb services. <br />
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
</AlertDescription>
</Alert>
);
}
@@ -1,25 +0,0 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
export function PageHeader() {
return (
<div className="flex flex-col gap-1.5">
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Account Abstraction
</h1>
<p className="text-muted-foreground text-sm">
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
into your apps.{" "}
<TrackedLinkTW
target="_blank"
label="docs-wallets"
category="smart-wallet"
href="https://portal.thirdweb.com/wallets/smart-wallet"
className="text-link-foreground hover:text-foreground"
>
View Documentation
</TrackedLinkTW>
</p>
</div>
);
}
@@ -1,79 +0,0 @@
import { accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
import { ConnectSDKCard } from "components/shared/ConnectSDKCard";
import { SmartWallets } from "components/smart-wallets";
import { redirect } from "next/navigation";
import { getAccount } from "../../../../../account/settings/getAccount";
import { AccountAbstractionAPIKeysMenu } from "../AccountAbstractionAPIKeysMenu";
import { OpChainAlert } from "../OpChainAlert";
import { PageHeader } from "../PageHeader";
import { getAASupportedAPIKeys } from "../getAASupportedAPIKeys";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
searchParams: Promise<{
tab?: string;
}>;
}) {
const { clientId } = await props.params;
const dashboardAccount = await getAccount();
if (!dashboardAccount) {
redirect(
`/login?next=${encodeURIComponent(
`/dashboard/connect/account-abstraction/${clientId}`,
)}`,
);
}
const apiKeys = await getAASupportedAPIKeys();
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/account-abstraction");
}
const hasSmartWalletsWithoutBilling = apiKeys.find((k) =>
k.services?.find(
(s) =>
dashboardAccount.status !== accountStatus.validPayment &&
s.name === "bundler",
),
);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col content-start justify-between gap-4 lg:flex-row">
<PageHeader />
<div>
<AccountAbstractionAPIKeysMenu
apiKeys={apiKeys.map((x) => ({
name: x.name,
key: x.key,
}))}
selectedAPIKey={apiKey}
/>
</div>
</div>
{hasSmartWalletsWithoutBilling ? (
<SmartWalletsBillingAlert />
) : (
<OpChainAlert />
)}
<SmartWallets
apiKeyServices={apiKey.services || []}
trackingCategory="smart-wallet"
tab={(await props.searchParams).tab}
smartWalletsLayoutSlug={`/dashboard/connect/account-abstraction/${clientId}`}
clientId={apiKey.key}
/>
<ConnectSDKCard
title="Get Started"
description="Add account abstraction to your app with the Connect SDK."
/>
</div>
);
}
@@ -1,7 +0,0 @@
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
export async function getAASupportedAPIKeys() {
return (await getApiKeys()).filter((key) => {
return !!(key.services || []).find((srv) => srv.name === "bundler");
});
}
@@ -1,29 +0,0 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { Metadata } from "next";
import { getAbsoluteUrl } from "../../../../../lib/vercel-utils";
export default function Layout(props: { children: React.ReactNode }) {
return <ChakraProviderSetup>{props.children}</ChakraProviderSetup>;
}
const seo = {
title: "The Complete Account Abstraction Toolkit | thirdweb",
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
};
export const metadata: Metadata = {
title: seo.title,
description: seo.desc,
openGraph: {
title: seo.title,
description: seo.desc,
images: [
{
url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
width: 1200,
height: 630,
alt: seo.title,
},
],
},
};
@@ -1,36 +0,0 @@
import { redirect } from "next/navigation";
import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys";
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { PageHeader } from "./PageHeader";
import { getAASupportedAPIKeys } from "./getAASupportedAPIKeys";
export default async function Page() {
const authToken = await getAuthToken();
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent("/dashboard/connect/account-abstraction")}`,
);
}
const apiKeys = await getAASupportedAPIKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/account-abstraction/${firstKey.key}`);
}
return (
<div>
<PageHeader />
<div className="h-8" />
<NoApiKeys service="Account Abstraction" />
<div className="h-10" />
<ConnectSDKCard
title="Get Started"
description="Add account abstraction to your app with the Connect SDK."
/>
</div>
);
}
@@ -1,21 +0,0 @@
"use client";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { useDashboardRouter } from "../../../../../../@/lib/DashboardRouter";
import { ApiKeysMenu } from "../../../../../../components/settings/ApiKeys/Menu";
export function AnalyticsPageAPIKeysMenu(props: {
apiKeys: Pick<ApiKey, "name" | "key">[];
selectedAPIKey: Pick<ApiKey, "name" | "key">;
}) {
const router = useDashboardRouter();
return (
<ApiKeysMenu
apiKeys={props.apiKeys}
selectedKey={props.selectedAPIKey}
onSelect={(key) => {
router.push(`/dashboard/connect/analytics/${key.key}`);
}}
/>
);
}
@@ -1,63 +0,0 @@
import { redirect } from "next/navigation";
import { ConnectSDKCard } from "../../../../../../components/shared/ConnectSDKCard";
import { getApiKeys } from "../../../../../api/lib/getAPIKeys";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { ConnectAnalyticsDashboard } from "../../../../../team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard";
import { AnalyticsPageAPIKeysMenu } from "./AnalyticsPageAPIKeysMenu";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
}) {
const authToken = await getAuthToken();
const { clientId } = await props.params;
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent(`/dashboard/connect/analytics/${clientId}`)}`,
);
}
const _apiKeys = await getApiKeys();
// slimmed down version to reduce client side payload
const apiKeys = _apiKeys.map((key) => ({
name: key.name,
key: key.key,
}));
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/analytics");
}
return (
<div>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
Connect Analytics
</h1>
<p className="text-muted-foreground">
Visualize how users are connecting to your apps.
</p>
</div>
<div>
<AnalyticsPageAPIKeysMenu apiKeys={apiKeys} selectedAPIKey={apiKey} />
</div>
</div>
<div className="h-4 lg:h-8" />
<ConnectAnalyticsDashboard
clientId={apiKey.key}
connectLayoutSlug="/dashboard/connect"
/>
<div className="h-4 lg:h-8" />
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,61 +0,0 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
// base page
// - if no keys -> show no keys page
// - else redirect to first key
export default async function Page() {
const authToken = await getAuthToken();
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent("/dashboard/connect/analytics")}`,
);
}
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/analytics/${firstKey.key}`);
}
return <NoKeysCreatedPage />;
}
function NoKeysCreatedPage() {
return (
<div>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
Connect Analytics
</h1>
<p className="text-muted-foreground">
Visualize how users are connecting to your apps.
</p>
</div>
</div>
<div className="h-4 lg:h-8" />
<div className="flex flex-col items-center rounded-lg border border-border bg-muted/50 px-4 py-10 lg:px-6">
<h3 className="mb-3 font-semibold text-2xl">No API keys found</h3>
<p className="mb-6 text-muted-foreground text-sm">
Start using the Connect SDK in your app with a free API key.
</p>
<Button asChild variant="primary">
<Link href="/dashboard/settings/api-keys">Create API Key</Link>
</Button>
</div>
<div className="h-4 lg:h-8" />
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
</div>
);
}
@@ -1,19 +0,0 @@
import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout";
export default async function Layout(props: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const params = await props.params;
const { children } = props;
return (
<EcosystemLayoutSlug
params={params}
ecosystemLayoutPath="/dashboard/connect/ecosystem"
>
{children}
</EcosystemLayoutSlug>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,7 +0,0 @@
import { EcosystemCreatePage } from "./EcosystemCreatePage";
export default function Page() {
return (
<EcosystemCreatePage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,7 +0,0 @@
import { EcosystemLandingPage } from "./EcosystemLandingPage";
export default function Page() {
return (
<EcosystemLandingPage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -1,24 +0,0 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
export function PageHeader() {
return (
<div>
<h1 className="font-semibold text-3xl tracking-tigher">In-App Wallets</h1>
<div className="h-3" />
<p className="max-w-[500px] text-muted-foreground ">
A wallet infrastructure that enables apps to create, manage, and control
their users wallets. Email login, social login, and bring-your-own auth
supported.{" "}
<TrackedLinkTW
target="_blank"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
label="learn-more"
category="embedded-wallet"
className="text-link-foreground hover:text-foreground"
>
Learn more
</TrackedLinkTW>
</p>
</div>
);
}
@@ -1,32 +0,0 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { usePathname } from "next/navigation";
export function Tabs({ clientId }: { clientId: string }) {
const path = usePathname();
return (
<TabLinks
links={[
{
name: "Analytics",
href: `/dashboard/connect/in-app-wallets/${clientId}/analytics`,
isActive: path?.endsWith("/analytics") || false,
isDisabled: false,
},
{
name: "Users",
href: `/dashboard/connect/in-app-wallets/${clientId}/users`,
isActive: path?.endsWith("/users") || false,
isDisabled: false,
},
{
name: "Configuration",
href: `/dashboard/connect/in-app-wallets/${clientId}/config`,
isActive: path?.endsWith("/config") || false,
isDisabled: false,
},
]}
/>
);
}
@@ -1 +0,0 @@
export const TRACKING_CATEGORY = "embedded-wallet";
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,37 +0,0 @@
import type { Range } from "components/analytics/date-range-selector";
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
export default async function Page(props: {
params: Promise<{ clientId: string }>;
searchParams: Promise<{
from?: string;
to?: string;
type?: string;
interval?: string;
}>;
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const range =
searchParams.from && searchParams.to
? {
type: searchParams.type ?? "last-120",
from: new Date(searchParams.from),
to: new Date(searchParams.to),
}
: undefined;
const interval: "day" | "week" = ["day", "week"].includes(
searchParams.interval ?? "",
)
? (searchParams.interval as "day" | "week")
: "week";
return (
<InAppWalletAnalytics
clientId={params.clientId}
interval={interval}
range={range as Range}
/>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,24 +0,0 @@
import { InAppWalletSettingsPage } from "components/embedded-wallets/Configure";
import { redirect } from "next/navigation";
import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
import { TRACKING_CATEGORY } from "../_constants";
export default async function Page(props: {
params: Promise<{ clientId: string }>;
}) {
const params = await props.params;
const { clientId } = params;
const apiKeys = await getInAppWalletSupportedAPIKeys();
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/in-app-wallets");
}
return (
<InAppWalletSettingsPage
apiKey={apiKey}
trackingCategory={TRACKING_CATEGORY}
/>
);
}
@@ -1,87 +0,0 @@
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
import { subDays } from "date-fns";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { InAppWaletFooterSection } from "../../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
import { PageHeader } from "../PageHeader";
import { getInAppWalletSupportedAPIKeys } from "../getInAppWalletSupportedAPIKeys";
import { InAppWalletsAPIKeysMenu } from "../inAppWalletsAPIKeysMenu";
import { Tabs } from "./_components/tabs";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
searchParams: Promise<{
tab?: string;
}>;
children: React.ReactNode;
}) {
const authToken = getAuthToken();
const { clientId } = await props.params;
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent(`/dashboard/connect/in-app-wallets/${clientId}`)}`,
);
}
const apiKeys = await getInAppWalletSupportedAPIKeys();
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/in-app-wallets");
}
const allTimeStatsPromise = getInAppWalletUsage({
clientId,
from: new Date(2022, 0, 1),
to: new Date(),
period: "all",
});
const monthlyStatsPromise = getInAppWalletUsage({
clientId,
from: subDays(new Date(), 30),
to: new Date(),
period: "month",
});
const [allTimeStats, monthlyStats] = await Promise.all([
allTimeStatsPromise,
monthlyStatsPromise,
]).catch(() => [null, null]);
return (
<div>
{/* header */}
<div className="flex flex-col gap-4 lg:flex-row lg:justify-between">
<PageHeader />
<div>
<InAppWalletsAPIKeysMenu
apiKeys={apiKeys.map((x) => ({
name: x.name,
key: x.key,
}))}
selectedAPIKey={apiKey}
/>
</div>
</div>
<div className="h-8" />
<InAppWalletsSummary
allTimeStats={allTimeStats || []}
monthlyStats={monthlyStats || []}
/>
<div className="h-8" />
<Tabs clientId={clientId} />
<div className="h-8" />
{props.children}
<div className="h-16" />
<InAppWaletFooterSection trackingCategory="embedded-wallet" />
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,11 +0,0 @@
import { redirect } from "next/navigation";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
}) {
const { clientId } = await props.params;
redirect(`/dashboard/connect/in-app-wallets/${clientId}/analytics`);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,24 +0,0 @@
import { InAppWalletUsersPageContent } from "components/embedded-wallets/Users";
import { redirect } from "next/navigation";
import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
import { TRACKING_CATEGORY } from "../_constants";
export default async function Page(props: {
params: Promise<{
clientId: string;
}>;
}) {
const { clientId } = await props.params;
const apiKeys = await getInAppWalletSupportedAPIKeys();
const apiKey = apiKeys.find((key) => key.key === clientId);
if (!apiKey) {
redirect("/dashboard/connect/in-app-wallets");
}
return (
<InAppWalletUsersPageContent
clientId={apiKey.key}
trackingCategory={TRACKING_CATEGORY}
/>
);
}
@@ -1,7 +0,0 @@
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
export async function getInAppWalletSupportedAPIKeys() {
return (await getApiKeys()).filter((key) => {
return !!(key.services || []).find((srv) => srv.name === "embeddedWallets");
});
}
@@ -1,21 +0,0 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { ApiKeysMenu } from "../../../../../components/settings/ApiKeys/Menu";
export function InAppWalletsAPIKeysMenu(props: {
apiKeys: Pick<ApiKey, "name" | "key">[];
selectedAPIKey: Pick<ApiKey, "name" | "key">;
}) {
const router = useDashboardRouter();
return (
<ApiKeysMenu
apiKeys={props.apiKeys}
selectedKey={props.selectedAPIKey}
onSelect={(key) => {
router.push(`/dashboard/connect/in-app-wallets/${key.key}`);
}}
/>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,33 +0,0 @@
import { redirect } from "next/navigation";
import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { InAppWaletFooterSection } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
import { PageHeader } from "./PageHeader";
import { getInAppWalletSupportedAPIKeys } from "./getInAppWalletSupportedAPIKeys";
export default async function Page() {
const authToken = await getAuthToken();
if (!authToken) {
redirect(
`/login?next=${encodeURIComponent("/dashboard/connect/in-app-wallets")}`,
);
}
const apiKeys = await getInAppWalletSupportedAPIKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/in-app-wallets/${firstKey.key}`);
}
return (
<div>
<PageHeader />
<div className="h-8" />
<NoApiKeys service="in-app wallets" />
<div className="h-16" />
<InAppWaletFooterSection trackingCategory="embedded-wallet" />
</div>
);
}
@@ -1,13 +0,0 @@
import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
import { ConnectSidebarLayout } from "./DashboardConnectLayout";
export default function Layout(props: {
children: React.ReactNode;
}) {
return (
<ConnectSidebarLayout>
<BillingAlerts className="pb-6" />
{props.children}
</ConnectSidebarLayout>
);
}
@@ -1,60 +0,0 @@
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { getApiKeys } from "../../../../../api/lib/getAPIKeys";
import { APIKeySelector } from "../components/APIKeySelector";
export default async function Layout(props: {
params: Promise<{
id: string;
}>;
children: React.ReactNode;
}) {
const params = await props.params;
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
if (!firstKey) {
redirect("/dashboard/connect/pay/no-keys");
}
const selectedKey = apiKeys.find((x) => x.id === params.id);
if (!selectedKey) {
redirect("/dashboard/connect/pay");
}
const layoutPath = `/dashboard/connect/pay/${selectedKey.id}`;
return (
<div>
<div className="relative flex w-full flex-col gap-4 lg:flex-row">
<div className="right-0 bottom-3 z-10 max-sm:w-full lg:absolute lg:min-w-[200px]">
<APIKeySelector apiKeys={apiKeys} selectedApiKey={selectedKey} />
</div>
<TabPathLinks
className="w-full"
links={[
{
name: "Analytics",
path: layoutPath,
exactMatch: true,
},
{
name: "Webhooks",
path: `${layoutPath}/webhooks`,
},
{
name: "Settings",
path: `${layoutPath}/settings`,
},
]}
/>
</div>
<div className="h-4" />
{props.children}
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,17 +0,0 @@
import { notFound } from "next/navigation";
import { PayAnalytics } from "../../../../../../components/pay/PayAnalytics/PayAnalytics";
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: Promise<{
id: string;
}>;
}) {
const apiKey = await getAPIKey((await props.params).id);
if (!apiKey) {
notFound();
}
return <PayAnalytics clientId={apiKey.key} />;
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,17 +0,0 @@
import { notFound } from "next/navigation";
import { PayConfig } from "../../../../../../../components/pay/PayConfig";
import { getAPIKey } from "../../../../../../api/lib/getAPIKeys";
export default async function Page(props: {
params: Promise<{
id: string;
}>;
}) {
const apiKey = await getAPIKey((await props.params).id);
if (!apiKey) {
notFound();
}
return <PayConfig apiKey={apiKey} />;
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,17 +0,0 @@
import { notFound } from "next/navigation";
import { getAPIKey } from "../../../../../../api/lib/getAPIKeys";
import { PayWebhooksPage } from "../../components/webhooks.client";
export default async function Page(props: {
params: Promise<{
id: string;
}>;
}) {
const apiKey = await getAPIKey((await props.params).id);
if (!apiKey) {
notFound();
}
return <PayWebhooksPage clientId={apiKey.key} />;
}
@@ -1,41 +0,0 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
export function APIKeySelector(props: {
apiKeys: ApiKey[];
selectedApiKey: ApiKey;
}) {
const router = useDashboardRouter();
return (
<Select
value={props.selectedApiKey.id}
onValueChange={(keyId) => {
const newKey = props.apiKeys.find((x) => x.id === keyId);
if (newKey) {
router.push(`/dashboard/connect/pay/${newKey.id}`);
}
}}
>
<SelectTrigger>
<SelectValue>{props.selectedApiKey.name}</SelectValue>
</SelectTrigger>
<SelectContent>
{props.apiKeys.map((key) => (
<SelectItem key={key.id} value={key.id}>
{key.name}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
@@ -1,31 +0,0 @@
import Link from "next/link";
export default async function Layout(props: {
children: React.ReactNode;
}) {
return (
<div className="flex w-full flex-col gap-8">
<div className="flex flex-col items-start justify-between gap-6 lg:flex-row">
<div className="max-w-[800px]">
<h1 className="mb-3 font-bold text-3xl tracking-tight lg:text-4xl">
Pay
</h1>
<p className="text-muted-foreground leading-7">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
via cross-chain routing.{" "}
<Link
target="_blank"
href="https://portal.thirdweb.com/connect/pay/overview"
className="!text-link-foreground"
>
Learn more
</Link>
</p>
</div>
</div>
{props.children}
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,5 +0,0 @@
import { NoApiKeys } from "../../../../../../components/settings/ApiKeys/NoApiKeys";
export default function Page() {
return <NoApiKeys service="Pay in Connect" />;
}
@@ -1,13 +0,0 @@
import { redirect } from "next/navigation";
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
export default async function Page() {
const apiKeys = await getApiKeys();
const firstKey = apiKeys[0];
if (firstKey) {
redirect(`/dashboard/connect/pay/${firstKey.id}`);
}
redirect("/dashboard/connect/pay/no-keys");
}
@@ -1,19 +0,0 @@
import { redirect } from "next/navigation";
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
import { DeployedContractsPage } from "../../../../team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage";
export default async function Page() {
const accountAddress = await getAuthTokenWalletAddress();
if (!accountAddress) {
return redirect(
`/login?next=${encodeURIComponent("/dashboard/contracts/deploy")}`,
);
}
return (
<DeployedContractsPage
address={accountAddress}
className="flex grow flex-col"
/>
);
}
@@ -1,13 +0,0 @@
import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
import { ContractsSidebarLayout } from "../../../../core-ui/sidebar/contracts";
export default function Layout(props: {
children: React.ReactNode;
}) {
return (
<ContractsSidebarLayout>
<BillingAlerts className="pb-6" />
{props.children}
</ContractsSidebarLayout>
);
}
@@ -1,64 +0,0 @@
import { Changelog, type ChangelogItem } from "components/dashboard/Changelog";
import { HomeProductCard } from "components/dashboard/HomeProductCard";
import { OnboardingSteps } from "components/onboarding/Steps";
import { PRODUCTS } from "components/product-pages/common/nav/data";
const TRACKING_CATEGORY = "dashboard";
export default async function Page() {
const changelog = await getChangelog();
return (
<div className="container flex flex-col justify-between gap-16 pt-8 pb-16 xl:flex-row">
<div className="grow">
<h1 className="mb-6 font-semibold text-2xl tracking-tight lg:mb-8 lg:text-3xl">
Get started quickly
</h1>
<div className="flex w-full flex-col gap-10">
<OnboardingSteps />
<div className="flex w-full flex-col gap-12">
{["connect", "contracts", "infrastructure"].map((section) => {
const products = PRODUCTS.filter(
(p) => p.section === section && !!p.dashboardLink,
);
return (
<div key={section}>
<h3 className="mb-2.5 font-medium text-muted-foreground text-xl capitalize tracking-tight">
{section === "infrastructure" ? "Engine" : section}
</h3>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{products.map((product) => (
<HomeProductCard
key={product.name}
product={product}
TRACKING_CATEGORY={TRACKING_CATEGORY}
/>
))}
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="shrink-0 lg:w-[320px]">
<h2 className="mb-4 font-semibold text-lg tracking-tight">
Latest changes
</h2>
<Changelog changelog={changelog} />
</div>
</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[];
}
// revalidate every 5 minutes
export const revalidate = 300;
@@ -1,9 +1,3 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { ContractRow } from "components/explore/contract-row";
import { DeployUpsellCard } from "components/explore/upsells/deploy-your-own";
import { PublishUpsellCard } from "components/explore/upsells/publish-submit";
@@ -23,13 +17,6 @@ export const metadata: Metadata = {
export default async function ExplorePage() {
return (
<div className="flex flex-col">
<Breadcrumb className="border-border border-b px-6 py-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>Explore</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container flex flex-col gap-4 py-8">
<h1 className="mb-3 font-bold text-3xl tracking-tighter lg:text-5xl">
Explore
@@ -1,12 +1,16 @@
import { AppFooter } from "@/components/blocks/app-footer";
import { ErrorProvider } from "../../contexts/error-handler";
import { DashboardHeader } from "../components/Header/DashboardHeader";
import { TeamHeader } from "../team/components/TeamHeader/team-header";
export default function DashboardLayout(props: { children: React.ReactNode }) {
export default function DashboardLayout(props: {
children: React.ReactNode;
}) {
return (
<ErrorProvider>
<div className="flex min-h-screen flex-col bg-background">
<DashboardHeader />
<div className="border-border border-b bg-muted/50">
<TeamHeader />
</div>
<div className="flex grow flex-col">{props.children}</div>
<AppFooter />
</div>
@@ -10,7 +10,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
if (!searchParams?.uri) {
// redirect back out if we do not have a uri
return redirect("/dashboard");
return redirect("/team");
}
const contractMetadata = await fetchDeployMetadata({
@@ -1,6 +1,5 @@
import { type SortBy, type TimeRange, fetchTopContracts } from "lib/search";
import type { Metadata } from "next";
import { ContractsSidebarLayout } from "../../../core-ui/sidebar/contracts";
import { TrendingContractSection } from "./components/trending-table";
export const metadata: Metadata = {
@@ -26,7 +25,7 @@ export default async function DashboardContractTrendingPage(props: {
});
return (
<ContractsSidebarLayout>
<div className="container py-8">
<h1 className="mb-5 font-semibold text-2xl tracking-tight md:text-3xl">
Trending Contracts
</h1>
@@ -35,6 +34,6 @@ export default async function DashboardContractTrendingPage(props: {
searchParams={await props.searchParams}
showPagination={true}
/>
</ContractsSidebarLayout>
</div>
);
}
@@ -2,10 +2,12 @@
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useCallback, useState } from "react";
import { useActiveWallet, useDisconnect } from "thirdweb/react";
import { LazyCreateAPIKeyDialog } from "../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import {
type AccountHeaderCompProps,
@@ -20,6 +22,9 @@ export function AccountHeader(props: {
const router = useDashboardRouter();
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
useState(false);
const client = useThirdwebClient();
const wallet = useActiveWallet();
const { disconnect } = useDisconnect();
const logout = useCallback(async () => {
// log out the user
@@ -27,11 +32,14 @@ export function AccountHeader(props: {
await fetch("/api/auth/logout", {
method: "POST",
});
router.push("/login");
if (wallet) {
disconnect(wallet);
}
router.refresh();
} catch (e) {
console.error("Failed to log out", e);
}
}, [router]);
}, [router, disconnect, wallet]);
const headerProps: AccountHeaderCompProps = {
teamsAndProjects: props.teamsAndProjects,
@@ -39,6 +47,7 @@ export function AccountHeader(props: {
connectButton: <CustomConnectWallet />,
createProject: () => setIsCreateProjectDialogOpen(true),
account: myAccountQuery.data,
client,
};
return (
@@ -49,7 +58,6 @@ export function AccountHeader(props: {
<LazyCreateAPIKeyDialog
open={isCreateProjectDialogOpen}
onOpenChange={setIsCreateProjectDialogOpen}
wording="project"
onCreateAndComplete={() => {
// refresh projects
router.refresh();
@@ -1,4 +1,5 @@
import { Button } from "@/components/ui/button";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { teamsAndProjectsStub } from "../../../stories/stubs";
@@ -36,6 +37,8 @@ export const Mobile: Story = {
},
};
const client = getThirdwebClient();
function Variants(props: {
type: "mobile" | "desktop";
}) {
@@ -55,6 +58,7 @@ function Variants(props: {
id: "foo",
email: "[email protected]",
}}
client={client}
/>
</BadgeContainer>
@@ -65,6 +69,7 @@ function Variants(props: {
connectButton={<ConnectButtonStub />}
createProject={() => {}}
account={undefined}
client={client}
/>
</BadgeContainer>
</div>
@@ -4,6 +4,7 @@ import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
import { cn } from "@/lib/utils";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import { SecondaryNav } from "../../components/Header/SecondaryNav/SecondaryNav";
import { MobileBurgerMenuButton } from "../../components/MobileBurgerMenuButton";
import { ThirdwebMiniLogo } from "../../components/ThirdwebMiniLogo";
@@ -17,6 +18,7 @@ export type AccountHeaderCompProps = {
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
createProject: () => void;
account: Pick<Account, "email" | "id"> | undefined;
client: ThirdwebClient;
};
export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
@@ -41,21 +43,24 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
>
{/* TODO - set account Image */}
<GradientAvatar
id={props.account?.id}
src={props.account ? "" : undefined}
id={props.account?.id || "default"}
src={""}
className="size-6"
client={props.client}
/>
<span> My Account </span>
</Link>
<TeamAndProjectSelectorPopoverButton
currentProject={undefined}
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
focus="team-selection"
createProject={props.createProject}
account={props.account}
/>
{props.teamsAndProjects.length > 0 && (
<TeamAndProjectSelectorPopoverButton
currentProject={undefined}
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
focus="team-selection"
createProject={props.createProject}
account={props.account}
/>
)}
</div>
</div>
@@ -63,6 +68,7 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
account={props.account}
logout={props.logout}
connectButton={props.connectButton}
client={props.client}
/>
</header>
);
@@ -89,16 +95,19 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
id={props.account?.id}
src={props.account ? "" : undefined}
className="size-6"
client={props.client}
/>
<span> My Account </span>
</Link>
<TeamSelectorMobileMenuButton
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
upgradeTeamLink={undefined}
account={props.account}
/>
{props.teamsAndProjects.length > 0 && (
<TeamSelectorMobileMenuButton
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
upgradeTeamLink={undefined}
account={props.account}
/>
)}
</div>
</div>
@@ -0,0 +1,57 @@
"use client";
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useQuery } from "@tanstack/react-query";
import { AccountHeader } from "./AccountHeader";
// Remove this file when AppShell is deleted
export function AccountHeaderClient() {
const accountQuery = useAccount();
const teamsQuery = useQuery({
queryKey: ["teams", accountQuery.data?.id],
queryFn: () => getTeamsClient(),
});
const teamsAndProjectsQuery = useQuery({
queryKey: [teamsQuery.data?.[0]?.slug, "projects"],
queryFn: async () => {
const teams = teamsQuery.data;
if (!teams) {
throw new Error("No teams");
}
const teamsAndProjects = await Promise.all(
teams.map(async (team) => ({
team,
projects: await getProjectsForTeamClient(team.slug),
})),
);
return teamsAndProjects;
},
enabled: !!teamsQuery.data,
});
return <AccountHeader teamsAndProjects={teamsAndProjectsQuery.data || []} />;
}
async function getTeamsClient() {
const res = await fetch("/api/server-proxy/api/v1/teams");
if (res.ok) {
return (await res.json()).result as Team[];
}
throw new Error("Failed to fetch teams");
}
async function getProjectsForTeamClient(teamSlug: string) {
const res = await fetch(
`/api/server-proxy/api/v1/teams/${teamSlug}/projects`,
);
if (res.ok) {
return (await res.json()).result as Project[];
}
throw new Error("Failed to fetch projects");
}
@@ -1,10 +1,10 @@
"use client";
import { Button } from "@/components/ui/button";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { DownloadIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ImportModal } from "../../../../../components/contract-components/import-contract/modal";
export function DeployedContractsPageHeader() {
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -1,6 +1,7 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert";
import { Suspense } from "react";
import { ClientOnly } from "../../../../../../components/ClientOnly/ClientOnly";
import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
import { DeployedContractsTable } from "./DeployedContractsTable";
import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
@@ -12,6 +13,7 @@ export function DeployedContractsPage(props: {
}) {
return (
<div className={props.className}>
<BillingAlerts className="mb-10" />
<DeployedContractsPageHeader />
<div className="h-6" />
<Suspense fallback={<Loading />}>
@@ -1,7 +1,7 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
import type { BasicContract } from "contract-ui/types/types";
import { DeployedContracts } from "../../../../../../components/contract-components/tables/deployed-contracts";
export function DeployedContractsTable(props: {
contracts: BasicContract[];
@@ -1,11 +1,11 @@
"use client";
import { TabButtons } from "@/components/ui/tabs";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { StepsCard } from "components/dashboard/StepsCard";
import { useTrack } from "hooks/analytics/useTrack";
import Image from "next/image";
import { useMemo, useState } from "react";
import { ImportModal } from "../../../../../../components/contract-components/import-contract/modal";
import { StepsCard } from "../../../../../../components/dashboard/StepsCard";
import { useTrack } from "../../../../../../hooks/analytics/useTrack";
export function GetStartedWithContractsDeploy() {
const steps = useMemo(
@@ -1,7 +1,7 @@
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
import type { BasicContract } from "contract-ui/types/types";
import { MULTICHAIN_REGISTRY_CONTRACT } from "../../../../../../constants/contracts";
import { getAllMultichainRegistry } from "../../../../../../dashboard-extensions/common/read/getAllMultichainRegistry";
import { fetchChain } from "../../../../../../utils/fetchChain";
import { getAllMultichainRegistry } from "../../../../dashboard-extensions/common/read/getAllMultichainRegistry";
import { fetchChain } from "../../../../utils/fetchChain";
export async function getSortedDeployedContracts(params: {
address: string;
@@ -0,0 +1,18 @@
import { redirect } from "next/navigation";
import { getAuthTokenWalletAddress } from "../../api/lib/getAuthToken";
import { DeployedContractsPage } from "./_components/DeployedContractsPage";
export default async function Page() {
const accountAddress = await getAuthTokenWalletAddress();
if (!accountAddress) {
return redirect(`/login?next=${encodeURIComponent("/account/contracts")}`);
}
return (
<DeployedContractsPage
address={accountAddress}
className="flex grow flex-col pt-10 pb-10"
/>
);
}
@@ -0,0 +1,37 @@
"use client";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { useAuthorizedWallets } from "@3rdweb-sdk/react/hooks/useApi";
import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert";
import { AuthorizedWalletsTable } from "components/settings/AuthorizedWallets/AuthorizedWalletsTable";
// TODO - remove ChakraProviderSetup after migrating AuthorizedWalletsTable
// TODO - fetch the authorized wallets server side
export default function Page() {
const authorizedWalletsQuery = useAuthorizedWallets();
return (
<div className="container py-8">
<BillingAlerts className="mb-10" />
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<div className="flex flex-col justify-between gap-4">
<h1 className="font-semibold text-3xl tracking-tight">Devices</h1>
<p className="text-muted-foreground">
List of authorized devices that can perform actions on behalf of
your account.
</p>
<ChakraProviderSetup>
<AuthorizedWalletsTable
authorizedWallets={authorizedWalletsQuery.data || []}
isPending={authorizedWalletsQuery.isPending}
isFetched={authorizedWalletsQuery.isFetched}
/>
</ChakraProviderSetup>
</div>
</div>
</div>
</div>
);
}
+31 -31
View File
@@ -1,9 +1,8 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { DashboardTypeCookieSetter } from "@/components/DashboardTypeCookieSetter";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { AppFooter } from "@/components/blocks/app-footer";
import type React from "react";
import { TabPathLinks } from "../../@/components/ui/tabs";
import { TWAutoConnect } from "../components/autoconnect";
import { AccountHeader } from "./components/AccountHeader";
@@ -14,36 +13,9 @@ export default async function AccountLayout(props: {
<div className="flex min-h-screen flex-col bg-background">
<div className="flex grow flex-col">
<HeaderAndNav />
<div className="border-border border-b py-10">
<div className="mx-auto max-w-[1100px] px-6">
<h1 className="font-semibold text-3xl tracking-tight">
My Account
</h1>
</div>
</div>
<SidebarLayout
className="max-w-[1100px]"
sidebarLinks={[
{
href: "/account",
label: "Overview",
exactMatch: true,
},
{
href: "/account/wallets",
label: "Wallets",
},
{
href: "/account/settings",
label: "Settings",
},
]}
>
{props.children}
</SidebarLayout>
{props.children}
</div>
<TWAutoConnect />
<DashboardTypeCookieSetter type="team" />
<AppFooter />
</div>
);
@@ -60,8 +32,36 @@ async function HeaderAndNav() {
);
return (
<div className="border-b bg-muted/50">
<div className="bg-muted/50">
<AccountHeader teamsAndProjects={teamsAndProjects} />
<TabPathLinks
tabContainerClassName="px-4 lg:px-6"
links={[
{
path: "/account",
name: "Overview",
exactMatch: true,
},
{
path: "/account/contracts",
name: "Contracts",
exactMatch: true,
},
// TODO - enable these links after they are functional
// {
// path: "/account/settings",
// name: "Settings",
// },
// {
// path: "/account/wallets",
// name: "Wallets",
// },
{
path: "/account/devices",
name: "Devices",
},
]}
/>
</div>
);
}
@@ -11,6 +11,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { EllipsisIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
@@ -97,14 +98,19 @@ function TeamRow(props: {
team: Team;
role: TeamAccountRole;
}) {
const client = useThirdwebClient();
const plan = getValidTeamPlan(props.team);
return (
<div className="flex items-center justify-between gap-2">
{/* start */}
<div className="flex items-center gap-4">
{/* TODO - set image */}
<GradientAvatar className="size-8" src={""} id={props.team.id} />
<GradientAvatar
className="size-8"
src={props.team.image}
id={props.team.id}
client={client}
/>
<div>
<div className="flex items-center gap-3">
+34 -6
View File
@@ -1,17 +1,45 @@
import { getTeams } from "@/api/team";
import { getMembers } from "@/api/team-members";
import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert";
import { redirect } from "next/navigation";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
import { getAccount } from "./settings/getAccount";
export default async function Page() {
const account = await getAccount();
if (!account) {
redirect("/login?next=/account");
}
const teams = await getTeams();
const teamsWithRole = teams.map((team) => ({
team,
// TODO fetch the role of current user in this team
role: "MEMBER" as const, // THIS IS A PLACEHOLDER !!!
}));
const teamsWithRole = (
await Promise.all(
teams.map(async (team) => {
const members = await getMembers(team.slug);
if (!members) {
return {
team,
role: "MEMBER" as const,
};
}
const accountMemberInfo = members.find(
(m) => m.accountId === account.id,
);
return {
team,
role: accountMemberInfo?.role || "MEMBER",
};
}),
)
).filter((x) => !!x);
return (
<div className="grow">
<div className="container grow py-8">
<BillingAlerts className="mb-10" />
<AccountTeamsUI teamsWithRole={teamsWithRole} />
</div>
);
@@ -1,6 +1,7 @@
"use client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert";
import type { ThirdwebClient } from "thirdweb";
import { upload } from "thirdweb/storage";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
@@ -10,24 +11,27 @@ export function AccountSettingsPage(props: {
client: ThirdwebClient;
}) {
return (
<AccountSettingsPageUI
account={props.account}
updateAccountImage={async (file) => {
if (file) {
// upload to IPFS
const ipfsUri = await upload({
client: props.client,
files: [file],
});
<div>
<BillingAlerts className="mb-10" />
<AccountSettingsPageUI
account={props.account}
updateAccountImage={async (file) => {
if (file) {
// upload to IPFS
const ipfsUri = await upload({
client: props.client,
files: [file],
});
// TODO - Implement updating the account image with uri
console.log(ipfsUri);
} else {
// TODO - Implement deleting the account image
}
// TODO - Implement updating the account image with uri
console.log(ipfsUri);
} else {
// TODO - Implement deleting the account image
}
throw new Error("Not implemented");
}}
/>
throw new Error("Not implemented");
}}
/>
</div>
);
}
@@ -1,8 +1,10 @@
import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert";
import { LinkWalletUI } from "./LinkWalletUI";
export default async function Page() {
return (
<div className="grow">
<BillingAlerts className="mb-10" />
<LinkWalletUI
// TODO - fetch the wallets of current user
wallets={[]}
+2 -2
View File
@@ -4,7 +4,7 @@ import { getAuthToken } from "./getAuthToken";
// TODO - Fix the `/v1/keys/${apiKeyId}` endpoint in API server
export async function getAPIKey(apiKeyId: string) {
async function getAPIKey(apiKeyId: string) {
// The `/v1/keys/${apiKeyId}`; does not return the "FULL" ApiKey object for some reason
// Until this is fixed in API server - we just use the getApiKeys() and find the key by id
@@ -35,7 +35,7 @@ export async function getAPIKey(apiKeyId: string) {
// return json.data as ApiKey;
}
export async function getApiKeys() {
async function getApiKeys() {
const authToken = await getAuthToken();
const res = await fetch(`${API_SERVER_URL}/v1/keys`, {
@@ -1,115 +0,0 @@
import { NavLink } from "@/components/ui/NavLink";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { CmdKSearch } from "components/cmd-k-search";
import { ColorModeToggle } from "components/color-mode/color-mode-toggle";
import { Logo } from "components/logo";
import { CreditsButton } from "components/settings/Account/Billing/CreditsButton";
import { UpgradeButton } from "components/settings/Account/Billing/UpgradeButton";
import { CircleHelpIcon } from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
import { DashboardHeaderTabs } from "./DashboardHeaderTabs";
export const DashboardHeader: React.FC = () => {
return (
<div className="bg-muted/50">
<header className="flex items-center justify-between px-4 py-3 lg:px-6">
{/* left */}
<div className="flex items-center gap-4">
<Link href="/dashboard">
<Logo hideWordmark />
</Link>
<CmdKSearch />
</div>
{/* Right */}
<div className="flex items-center gap-2">
<NavLink
href="/team"
tracking={{
category: "header",
action: "click",
label: "team-dashboard",
}}
className="hidden items-center gap-1.5 px-2.5 text-muted-foreground text-sm hover:text-foreground md:inline-flex"
activeClassName="text-foreground"
>
Team Dashboard <Badge className="px-1.5"> BETA </Badge>
</NavLink>
<div className="hidden gap-2 md:flex">
<Suspense fallback={null}>
<CreditsButton />
</Suspense>
<UpgradeButton />
</div>
<HeaderNavLink
trackingLabel="chainlist"
href="/chainlist"
label="Chainlist"
/>
<HeaderNavLink
trackingLabel="docs"
href="https://portal.thirdweb.com"
label="Docs"
/>
<HeaderNavLink
trackingLabel="support"
href="/support"
label="Support"
/>
<div className="md:hidden">
<Button asChild variant="ghost">
<NavLink
className="fade-in-0 !h-auto !w-auto p-2"
href="/support"
tracking={{
category: "header",
action: "click",
label: "support",
}}
>
<CircleHelpIcon className="size-5" />
</NavLink>
</Button>
</div>
<ColorModeToggle />
<div className="md:ml-2">
<CustomConnectWallet />
</div>
</div>
</header>
<DashboardHeaderTabs />
</div>
);
};
function HeaderNavLink(props: {
label: string;
trackingLabel: string;
href: string;
}) {
return (
<NavLink
href={props.href}
tracking={{
category: "header",
action: "click",
label: props.trackingLabel,
}}
className="hidden px-2.5 text-muted-foreground text-sm hover:text-foreground md:block"
activeClassName="text-foreground"
>
{props.label}
</NavLink>
);
}
@@ -1,42 +0,0 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { usePathname } from "next/navigation";
export function DashboardHeaderTabs() {
const pathname = usePathname() || "";
return (
<TabLinks
className="w-full"
tabContainerClassName="px-4"
shadowColor="transparent"
links={[
{
href: "/dashboard",
name: "Home",
isActive: pathname === "/dashboard",
},
{
href: "/dashboard/connect/analytics",
name: "Connect",
isActive: pathname.startsWith("/dashboard/connect"),
},
{
href: "/dashboard/contracts/deploy",
name: "Contracts",
isActive: pathname.startsWith("/dashboard/contracts"),
},
{
href: "/dashboard/engine",
name: "Engine",
isActive: pathname.startsWith("/dashboard/engine"),
},
{
href: "/dashboard/settings/api-keys",
name: "Settings",
isActive: pathname.startsWith("/dashboard/settings"),
},
]}
/>
);
}
@@ -55,7 +55,7 @@ export function ResourcesDropdownButton() {
<DropdownMenuItem>
<Link href="/trending" className="w-full p-1">
Popular Contracts
Trending Contracts
</Link>
</DropdownMenuItem>
@@ -1,8 +1,7 @@
import { ToolTipLabel } from "@/components/ui/tooltip";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { ArrowUpDownIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import type { ThirdwebClient } from "thirdweb";
import { ResourcesDropdownButton } from "./ResourcesDropdownButton";
import { AccountButton } from "./account-button.client";
@@ -10,6 +9,7 @@ export function SecondaryNav(props: {
account: Pick<Account, "email" | "id"> | undefined;
logout: () => void;
connectButton: React.ReactNode;
client: ThirdwebClient;
}) {
return (
<div className="flex items-center gap-6">
@@ -18,6 +18,7 @@ export function SecondaryNav(props: {
logout={props.logout}
connectButton={props.connectButton}
account={props.account}
client={props.client}
/>
</div>
);
@@ -26,23 +27,6 @@ export function SecondaryNav(props: {
export function SecondaryNavLinks() {
return (
<div className="flex items-center gap-6">
<ToolTipLabel
label={
<span>
You are exploring the new dashboard. <br /> Click here to go back to
the legacy dashboard.
</span>
}
>
<Link
href="/dashboard"
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
>
<ArrowUpDownIcon className="size-4" />
Switch To Legacy Dashboard
</Link>
</ToolTipLabel>
<ResourcesDropdownButton />
<Link
@@ -7,105 +7,119 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { LogOutIcon, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebClient } from "thirdweb";
export function AccountButton(props: {
logout: () => void;
connectButton: React.ReactNode;
account?: Pick<Account, "email" | "id">;
client: ThirdwebClient;
}) {
const { setTheme, theme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
return (
<DropdownMenu>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
size="icon"
asChild
className="size-10 rounded-full hover:ring-2 hover:ring-offset-2"
className="size-10 cursor-pointer rounded-full hover:ring-2 hover:ring-offset-2"
variant="ghost"
>
<button type="button">
{/* Don't remove the div */}
<div>
{/* TODO - set account image */}
<GradientAvatar
id={props.account?.id}
id={props.account?.id || "default"}
src={""}
className="size-9"
client={props.client}
/>
</button>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[300px] rounded-lg p-0" align="end">
<DropdownMenuContent
className="w-[300px] rounded-lg p-0"
align="end"
onClick={(e) => {
if (e.target instanceof HTMLAnchorElement) {
setIsOpen(false);
}
}}
>
<div className="border-border border-b p-4 pb-5">
<SkeletonContainer
skeletonData="[email protected]"
loadedData={props.account?.email}
render={(v) => <p className="text-muted-foreground text-sm">{v}</p>}
/>
{props.account?.email && (
<p className="text-muted-foreground text-sm">
{props.account.email}
</p>
)}
<div className="h-3" />
<div className="[&>*]:!w-full">{props.connectButton}</div>
</div>
<div className="flex flex-col gap-4 px-4 py-5">
<Link
href="/account"
className="text-muted-foreground text-sm hover:text-foreground"
<div className="flex flex-col gap-1 p-2">
<Button
asChild
variant="ghost"
className="justify-start px-3 text-muted-foreground text-sm hover:text-foreground"
>
Account Settings
</Link>
<div className="flex items-center justify-between gap-2">
<p className="text-muted-foreground text-sm">Theme</p>
<div className="ml-auto flex items-center gap-1 rounded-lg border border-border px-2 py-0.5">
<Button
size="icon"
onClick={() => setTheme("light")}
variant="ghost"
aria-label="Light theme"
className={cn(
"!p-1 !h-auto !w-auto",
theme === "light" ? "opacity-100" : "opacity-30",
)}
>
<Sun className="size-4" />
</Button>
<Button
size="icon"
onClick={() => setTheme("dark")}
variant="ghost"
aria-label="Dark theme"
className={cn(
"!p-1 !h-auto !w-auto",
theme === "dark" ? "opacity-100" : "opacity-30",
)}
>
<Moon className="size-4" />
</Button>
</div>
</div>
<Link
href="/"
className="text-muted-foreground text-sm hover:text-foreground"
>
Home Page
</Link>
<Link href="/account">My Account</Link>
</Button>
<Button
variant="link"
className="!h-auto hover:!no-underline justify-start gap-2 px-0 py-1 text-start text-muted-foreground hover:text-foreground"
onClick={props.logout}
variant="ghost"
className="flex items-center justify-between gap-2 px-3 text-muted-foreground text-sm hover:text-foreground"
onClick={() => {
setTheme(theme === "light" ? "dark" : "light");
}}
>
<LogOutIcon className="size-4" />
Log Out
<p className="text-sm">Theme</p>
<div className="ml-auto flex items-center gap-2 rounded-lg border border-border px-3 py-1.5 text-foreground">
<Sun
className={cn(
"size-4",
theme === "light" ? "opacity-100" : "opacity-30",
)}
/>
<Moon
className={cn(
"size-4",
theme === "dark" ? "opacity-100" : "opacity-30",
)}
/>
</div>
</Button>
<Button
asChild
variant="ghost"
className="justify-start px-3 text-muted-foreground text-sm hover:text-foreground"
>
<Link href="/">Home Page</Link>
</Button>
{props.account && (
<Button
variant="ghost"
onClick={() => {
props.logout();
setIsOpen(false);
}}
className="justify-between gap-2 px-3 text-muted-foreground hover:text-foreground"
>
Log Out
<LogOutIcon className="size-4" />
</Button>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
@@ -8,8 +8,8 @@ import {
LogOutIcon,
MenuIcon,
Moon,
SettingsIcon,
Sun,
UserRoundIcon,
XIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
@@ -96,8 +96,8 @@ export function MobileBurgerMenuButton(
href="/account"
className="flex items-center gap-2 text-base text-muted-foreground hover:text-foreground "
>
<SettingsIcon className="size-4" />
Account Settings
<UserRoundIcon className="size-4" />
My Account
</Link>
<Button
@@ -154,7 +154,7 @@ export function MobileBurgerMenuButton(
href="/trending"
className="text-muted-foreground hover:text-foreground "
>
Popular Contracts
Trending Contracts
</Link>
<Link
+1 -7
View File
@@ -7,7 +7,6 @@ import { useTheme } from "next-themes";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { ConnectEmbed } from "thirdweb/react";
import { getCookie } from "../../stores/SyncStoreToCookies";
import { ThirdwebMiniLogo } from "../components/ThirdwebMiniLogo";
import { getSDKTheme } from "../components/sdk-component-theme";
import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
@@ -49,12 +48,7 @@ function CustomConnectEmmbed() {
if (nextSearchParam && isValidRedirectPath(nextSearchParam)) {
router.replace(nextSearchParam);
} else {
const dashboardType = getCookie("x-dashboard-type");
if (dashboardType === "team") {
router.replace("/team");
} else {
router.replace("/dashboard");
}
router.replace("/team");
}
}

Some files were not shown because too many files have changed in this diff Show More