From 89af885d95d017320c492166ae7cfd22933cec04 Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Wed, 27 May 2026 12:31:14 -0700 Subject: [PATCH] Add dashboard user menu button. Co-authored-by: Cursor --- .../src/components/dashboard-user-button.tsx | 184 ++++++++++++++++++ apps/dashboard/src/components/navbar.tsx | 4 +- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 apps/dashboard/src/components/dashboard-user-button.tsx diff --git a/apps/dashboard/src/components/dashboard-user-button.tsx b/apps/dashboard/src/components/dashboard-user-button.tsx new file mode 100644 index 000000000..8152e6ffe --- /dev/null +++ b/apps/dashboard/src/components/dashboard-user-button.tsx @@ -0,0 +1,184 @@ +"use client"; + +import { + Avatar, + AvatarFallback, + AvatarImage, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Skeleton, + cn, +} from "@/components/ui"; +import { SignInIcon, SignOutIcon, SunIcon, UserCircleIcon, UserPlusIcon } from "@phosphor-icons/react"; +import { useStackApp, useUser } from "@stackframe/stack"; +import { Suspense } from "react"; + +type DashboardUserButtonProps = { + showUserInfo?: boolean, + colorModeToggle?: () => void | Promise, + extraItems?: { + text: string, + icon: React.ReactNode, + onClick: () => void | Promise, + }[], +}; + +type DashboardMenuItemProps = { + text: string, + icon: React.ReactNode, + onClick: () => void | Promise, + variant?: "default" | "destructive", +}; + +const menuIconClassName = "h-4 w-4 shrink-0 text-muted-foreground"; +const destructiveItemClasses = + "text-red-600 dark:text-red-400 focus:bg-red-500/10 data-[highlighted]:bg-red-500/10 dark:focus:bg-red-500/15 dark:data-[highlighted]:bg-red-500/15"; + +function DashboardUserAvatar(props: { + size?: number, + user: ReturnType, +}) { + const size = props.size ?? 34; + const user = props.user; + const label = user?.displayName ?? user?.primaryEmail ?? "User"; + const initials = label.slice(0, 2).toUpperCase(); + + return ( + + + + + {initials} + + + + ); +} + +function DashboardMenuItem(props: DashboardMenuItemProps) { + return ( + + {props.text} + + ); +} + +export function DashboardUserButton(props: DashboardUserButtonProps) { + return ( + }> + + + ); +} + +function DashboardUserButtonInner(props: DashboardUserButtonProps) { + const user = useUser(); + const app = useStackApp(); + const showUserInfo = props.showUserInfo === true; + const displayName = user?.displayName ?? user?.primaryEmail ?? "Account"; + const iconProps = { size: 16, className: menuIconClassName }; + + return ( + + +
+ + {user && showUserInfo && ( +
+
{displayName}
+ {user.primaryEmail != null && user.primaryEmail !== displayName && ( +
{user.primaryEmail}
+ )} +
+ )} +
+
+ + +
+ +
+ {user ? ( + <> +

{displayName}

+

{user.primaryEmail}

+ + ) : ( +

Not signed in

+ )} +
+
+
+ + {user && ( + await app.redirectToAccountSettings()} + icon={} + /> + )} + {!user && ( + await app.redirectToSignIn()} + icon={} + /> + )} + {!user && ( + await app.redirectToSignUp()} + icon={} + /> + )} + {user && props.extraItems?.map((item, index) => ( + + ))} + {props.colorModeToggle && ( + } + /> + )} + {user && ( + <> + + await user.signOut()} + icon={} + /> + + )} +
+
+ ); +} diff --git a/apps/dashboard/src/components/navbar.tsx b/apps/dashboard/src/components/navbar.tsx index 60d91a04e..d678d8f7f 100644 --- a/apps/dashboard/src/components/navbar.tsx +++ b/apps/dashboard/src/components/navbar.tsx @@ -2,8 +2,8 @@ import { Typography } from "@/components/ui"; import { getPublicEnvVar } from "@/lib/env"; -import { UserButton } from "@stackframe/stack"; +import { DashboardUserButton } from "./dashboard-user-button"; import { Link } from "./link"; import { Logo } from "./logo"; import ThemeToggle from "./theme-toggle"; @@ -26,7 +26,7 @@ export function Navbar({ ...props }) { - {!isRemoteDevelopmentEnvironment && } + {!isRemoteDevelopmentEnvironment && } );