add avatar menu in side bar

This commit is contained in:
KMKoushik
2025-05-07 23:05:46 +10:00
parent 065e0518fb
commit a47e524f4c
6 changed files with 253 additions and 12 deletions

View File

@@ -19,6 +19,13 @@ import {
BookOpenText, BookOpenText,
ChartColumnBig, ChartColumnBig,
ChartArea, ChartArea,
BellIcon,
CreditCardIcon,
LogOutIcon,
MoreVerticalIcon,
UserCircleIcon,
UsersIcon,
GaugeIcon,
} from "lucide-react"; } from "lucide-react";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
@@ -41,6 +48,16 @@ import { useSession } from "next-auth/react";
import { isSelfHosted } from "~/utils/common"; import { isSelfHosted } from "~/utils/common";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Badge } from "@unsend/ui/src/badge"; import { Badge } from "@unsend/ui/src/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@unsend/ui/src/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@unsend/ui/src/dropdown-menu";
// General items // General items
const generalItems = [ const generalItems = [
@@ -211,12 +228,6 @@ export function AppSidebar() {
<SidebarFooter> <SidebarFooter>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={() => signOut()} tooltip="Logout">
<LogOut />
<span>Logout</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Docs"> <SidebarMenuButton asChild tooltip="Docs">
<Link href="https://docs.unsend.dev" target="_blank"> <Link href="https://docs.unsend.dev" target="_blank">
@@ -225,12 +236,116 @@ export function AppSidebar() {
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
<SidebarMenuItem className="px-2">
<ThemeSwitcher />
</SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
<NavUser
user={{
name: session?.user.name || "",
email: session?.user.email || "",
avatar: session?.user.image || "",
}}
/>
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
); );
} }
export function NavUser({
user,
}: {
user: {
name?: string | null;
email?: string | null;
avatar?: string | null;
};
}) {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
{user.avatar ? (
<AvatarImage
src={user.avatar}
alt={user.name ?? user.email ?? ""}
/>
) : null}
<AvatarFallback className="rounded-lg capitalize">
{user.name?.charAt(0) ?? user.email?.charAt(0) ?? ""}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">
{user.name ?? user.email ?? ""}
</span>
<span className="truncate text-xs text-muted-foreground">
{user.name ? user.email : ""}
</span>
</div>
<MoreVerticalIcon className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-xl"
side={isMobile ? "bottom" : "top"}
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
{user.avatar ? (
<AvatarImage
src={user.avatar}
alt={user.name ?? user.email ?? ""}
/>
) : null}
<AvatarFallback className="rounded-lg capitalize">
{user.name?.charAt(0) ?? user.email?.charAt(0) ?? ""}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">
{user.name ?? user.email ?? ""}
</span>
<span className="truncate text-xs text-muted-foreground">
{user.name ? user.email : ""}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/settings/team">
<UsersIcon />
Team
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings">
<GaugeIcon />
Usage
</Link>
</DropdownMenuItem>
<div className="px-2 py-0.5">
<ThemeSwitcher />
</div>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>
<LogOutIcon />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -7,7 +7,7 @@ export const ThemeSwitcher = () => {
return ( return (
<div className="flex gap-2 items-center justify-between"> <div className="flex gap-2 items-center justify-between">
<p className="text-sm text-sidebar-foreground flex items-center gap-2"> <p className="text-sm text-popover-foreground flex items-center gap-2">
<SunMoonIcon className="h-4 w-4" /> <SunMoonIcon className="h-4 w-4" />
Theme Theme
</p> </p>

View File

@@ -31,6 +31,7 @@
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.8", "@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",

View File

@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "../lib/utils";
const Avatar = React.forwardRef<
React.ComponentRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ComponentRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ComponentRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -12,7 +12,7 @@
--card-foreground: 222.2 84% 4.9%; --card-foreground: 222.2 84% 4.9%;
--popover: 220 2% 96%; --popover: 220 2% 96%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 234 16% 35%;
--primary: 200 65% 14%; --primary: 200 65% 14%;
--primary-foreground: 210 40% 98%; --primary-foreground: 210 40% 98%;
@@ -62,7 +62,7 @@
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 240 21% 15%; --popover: 240 21% 15%;
--popover-foreground: 210 40% 98%; --popover-foreground: 226 64% 88%;
--primary: 220 23% 95%; --primary: 220 23% 95%;
--primary-foreground: 240 23% 9%; --primary-foreground: 240 23% 9%;

75
pnpm-lock.yaml generated
View File

@@ -527,6 +527,9 @@ importers:
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.2.8 specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0) version: 1.2.8(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0)
'@radix-ui/react-avatar':
specifier: ^1.1.9
version: 1.1.9(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0)
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.1.11 specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0) version: 1.1.11(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0)
@@ -4023,6 +4026,30 @@ packages:
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
dev: false dev: false
/@radix-ui/react-avatar@1.1.9(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0):
resolution: {integrity: sha512-10tQokfvZdFvnvDkcOJPjm2pWiP8A0R4T83MoD7tb15bC/k2GU7B1YBuzJi8lNQ8V1QqhP8ocNqp27ByZaNagQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
dev: false
/@radix-ui/react-collapsible@1.1.1(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0): /@radix-ui/react-collapsible@1.1.1(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0):
resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==}
peerDependencies: peerDependencies:
@@ -4725,6 +4752,26 @@ packages:
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
dev: false dev: false
/@radix-ui/react-primitive@2.1.2(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0):
resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-slot': 1.2.2(@types/react@19.1.2)(react@19.1.0)
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
dev: false
/@radix-ui/react-progress@1.1.4(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0): /@radix-ui/react-progress@1.1.4(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0):
resolution: {integrity: sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==} resolution: {integrity: sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==}
peerDependencies: peerDependencies:
@@ -4889,6 +4936,20 @@ packages:
react: 19.1.0 react: 19.1.0
dev: false dev: false
/@radix-ui/react-slot@1.2.2(@types/react@19.1.2)(react@19.1.0):
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@types/react': 19.1.2
react: 19.1.0
dev: false
/@radix-ui/react-switch@1.2.2(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0): /@radix-ui/react-switch@1.2.2(@types/react-dom@19.1.2)(@types/react@19.1.2)(react-dom@19.1.0)(react@19.1.0):
resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==} resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==}
peerDependencies: peerDependencies:
@@ -5116,6 +5177,20 @@ packages:
react: 19.1.0 react: 19.1.0
dev: false dev: false
/@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.2)(react@19.1.0):
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 19.1.2
react: 19.1.0
use-sync-external-store: 1.5.0(react@19.1.0)
dev: false
/@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.2)(react@19.1.0): /@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.2)(react@19.1.0):
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies: peerDependencies: