🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import { Link, useLocation } from "@tanstack/react-router";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@workspace/ui/components/breadcrumb";
import { Separator } from "@workspace/ui/components/separator";
import { SidebarTrigger } from "@workspace/ui/components/sidebar";
import { LanguageSwitch } from "@workspace/ui/composed/language-switch";
import { ThemeSwitch } from "@workspace/ui/composed/theme-switch";
import { Fragment, useMemo } from "react";
import { findNavByUrl, useNavs } from "./navs";
import TimezoneSwitch from "./timezone-switch";
import { UserNav } from "./user-nav";
export function Header() {
const pathname = useLocation({ select: (location) => location.pathname });
const navs = useNavs();
const items = useMemo(() => findNavByUrl(navs, pathname), [pathname]);
return (
<header className="sticky top-0 z-50 flex h-14 shrink-0 items-center gap-2 bg-background">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator className="mr-2 h-4" orientation="vertical" />
<Breadcrumb>
<BreadcrumbList>
{items.map((item, index) => (
<Fragment key={item?.title}>
{index !== items.length - 1 && (
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={item?.url || "/dashboard"}>{item?.title}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
)}
{index < items.length - 1 && <BreadcrumbSeparator />}
{index === items.length - 1 && (
<BreadcrumbPage>{item?.title}</BreadcrumbPage>
)}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex items-center gap-2 px-3">
<LanguageSwitch />
<TimezoneSwitch />
<ThemeSwitch />
<UserNav />
</div>
</header>
);
}
+32
View File
@@ -0,0 +1,32 @@
import { Outlet } from "@tanstack/react-router";
import {
SidebarInset,
SidebarProvider,
} from "@workspace/ui/components/sidebar";
import { getCookie } from "@workspace/ui/lib/cookies";
import { useEffect, useState } from "react";
import { Header } from "@/layout/header";
import { SidebarLeft } from "./sidebar-left";
export default function DashboardLayout() {
const [open, setOpen] = useState(true);
useEffect(() => {
const sidebarState = getCookie("sidebar_state");
if (sidebarState !== undefined) {
setOpen(sidebarState === "true");
}
}, []);
return (
<SidebarProvider defaultOpen={open}>
<SidebarLeft />
<SidebarInset className="relative flex-grow overflow-hidden">
<Header />
<div className="h-[calc(100vh-56px)] flex-grow gap-4 overflow-auto p-4">
<Outlet />
</div>
</SidebarInset>
</SidebarProvider>
);
}
+226
View File
@@ -0,0 +1,226 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export interface NavItem {
title: string;
url?: string;
icon?: string;
items?: NavItem[];
defaultOpen?: boolean;
}
export function useNavs() {
const { t } = useTranslation("menu");
const navs: NavItem[] = useMemo(
() => [
{
title: t("Dashboard", "Dashboard"),
url: "/dashboard",
icon: "flat-color-icons:globe",
},
{
title: t("Maintenance", "Maintenance"),
icon: "flat-color-icons:data-protection",
items: [
{
title: t("Server Management", "Server Management"),
url: "/dashboard/servers",
icon: "flat-color-icons:data-protection",
},
{
title: t("Node Management", "Node Management"),
url: "/dashboard/nodes",
icon: "flat-color-icons:mind-map",
},
{
title: t("Subscribe Config", "Subscribe Config"),
url: "/dashboard/subscribe",
icon: "flat-color-icons:ruler",
},
{
title: t("Product Management", "Product Management"),
url: "/dashboard/product",
icon: "flat-color-icons:shop",
},
],
},
{
title: t("Commerce", "Commerce"),
icon: "flat-color-icons:sales-performance",
items: [
{
title: t("Order Management", "Order Management"),
url: "/dashboard/order",
icon: "flat-color-icons:todo-list",
},
{
title: t("Coupon Management", "Coupon Management"),
url: "/dashboard/coupon",
icon: "flat-color-icons:bookmark",
},
{
title: t("Marketing Management", "Marketing Management"),
url: "/dashboard/marketing",
icon: "flat-color-icons:bullish",
},
{
title: t("Announcement Management", "Announcement Management"),
url: "/dashboard/announcement",
icon: "flat-color-icons:advertising",
},
],
},
{
title: t("Users & Support", "Users & Support"),
icon: "flat-color-icons:collaboration",
items: [
{
title: t("User Management", "User Management"),
url: "/dashboard/user",
icon: "flat-color-icons:conference-call",
},
{
title: t("Ticket Management", "Ticket Management"),
url: "/dashboard/ticket",
icon: "flat-color-icons:collaboration",
},
{
title: t("Document Management", "Document Management"),
url: "/dashboard/document",
icon: "flat-color-icons:document",
},
],
},
{
defaultOpen: false,
title: t("System", "System"),
icon: "flat-color-icons:services",
items: [
{
title: t("System Config", "System Config"),
url: "/dashboard/system",
icon: "flat-color-icons:services",
},
{
title: t("Auth Control", "Auth Control"),
url: "/dashboard/auth-control",
icon: "flat-color-icons:lock-portrait",
},
{
title: t("Payment Config", "Payment Config"),
url: "/dashboard/payment",
icon: "flat-color-icons:currency-exchange",
},
{
title: t("ADS Config", "ADS Config"),
url: "/dashboard/ads",
icon: "flat-color-icons:electrical-sensor",
},
],
},
{
defaultOpen: false,
title: t("Logs & Analytics", "Logs & Analytics"),
icon: "flat-color-icons:statistics",
items: [
{
title: t("Login", "Login"),
url: "/dashboard/log/login",
icon: "flat-color-icons:unlock",
},
{
title: t("Register", "Register"),
url: "/dashboard/log/register",
icon: "flat-color-icons:contacts",
},
{
title: t("Email", "Email"),
url: "/dashboard/log/email",
icon: "flat-color-icons:feedback",
},
{
title: t("Mobile", "Mobile"),
url: "/dashboard/log/mobile",
icon: "flat-color-icons:sms",
},
{
title: t("Subscribe", "Subscribe"),
url: "/dashboard/log/subscribe",
icon: "flat-color-icons:workflow",
},
{
title: t("Reset Subscribe", "Reset Subscribe"),
url: "/dashboard/log/reset-subscribe",
icon: "flat-color-icons:refresh",
},
{
title: t("Subscribe Traffic", "Subscribe Traffic"),
url: "/dashboard/log/subscribe-traffic",
icon: "flat-color-icons:statistics",
},
{
title: t("Server Traffic", "Server Traffic"),
url: "/dashboard/log/server-traffic",
icon: "flat-color-icons:statistics",
},
{
title: t("Traffic Details", "Traffic Details"),
url: "/dashboard/log/traffic-details",
icon: "flat-color-icons:combo-chart",
},
{
title: t("Balance", "Balance"),
url: "/dashboard/log/balance",
icon: "flat-color-icons:sales-performance",
},
{
title: t("Commission", "Commission"),
url: "/dashboard/log/commission",
icon: "flat-color-icons:debt",
},
{
title: t("Gift", "Gift"),
url: "/dashboard/log/gift",
icon: "flat-color-icons:donate",
},
],
},
],
[t]
);
return navs;
}
export function findNavByUrl(navs: NavItem[], url: string) {
function matchDynamicRoute(pattern: string, path: string): boolean {
const regexPattern = pattern
.replace(/:[^/]+/g, "[^/]+")
.replace(/\//g, "\\/");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
function findNav(
items: NavItem[],
url: string,
path: NavItem[] = []
): NavItem[] {
for (const item of items) {
if (item.url === url || (item.url && matchDynamicRoute(item.url, url))) {
return [...path, item];
}
if (item.items) {
const result = findNav(item.items, url, [...path, item]);
if (result.length) return result;
}
}
return [];
}
return findNav(navs, url);
}
+294
View File
@@ -0,0 +1,294 @@
import { Link, useLocation } from "@tanstack/react-router";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@workspace/ui/components/hover-card";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@workspace/ui/components/sidebar";
import { Icon } from "@workspace/ui/composed/icon";
import { cn } from "@workspace/ui/lib/utils";
import React, { useState } from "react";
import { useGlobalStore } from "@/stores/global";
import { type NavItem, useNavs } from "./navs";
function hasChildren(obj: any): obj is { items: any[] } {
return (
obj && Array.isArray((obj as any).items) && (obj as any).items.length > 0
);
}
export function SidebarLeft({
...props
}: React.ComponentProps<typeof Sidebar>) {
const { common } = useGlobalStore();
const { site } = common;
const navs = useNavs();
const pathname = useLocation({ select: (location) => location.pathname });
const { state, isMobile } = useSidebar();
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
React.useEffect(() => {
setOpenGroups((prev) => {
const next: Record<string, boolean> = { ...prev };
navs.forEach((nav) => {
if (hasChildren(nav) && next[nav.title] === undefined) {
next[nav.title] = nav.defaultOpen ?? true;
}
});
return next;
});
}, [navs]);
const handleToggleGroup = (title: string) => {
setOpenGroups((prev) => ({ ...prev, [title]: !prev[title] }));
};
const normalize = (p: string) =>
p.endsWith("/") && p !== "/" ? p.replace(/\/+$/, "") : p;
const isActiveUrl = (url: string) => {
const path = normalize(pathname);
const target = normalize(url);
if (target === "/dashboard") return path === target;
if (path === target) return true;
return path.startsWith(`${target}/`);
};
const isGroupActive = (nav: NavItem) =>
(hasChildren(nav) && nav.items?.some((i: any) => isActiveUrl(i.url))) ||
("url" in nav && nav.url ? isActiveUrl(nav.url as string) : false);
React.useEffect(() => {
setOpenGroups((prev) => {
const next: Record<string, boolean> = { ...prev };
navs.forEach((nav) => {
if (hasChildren(nav) && isGroupActive(nav)) next[nav.title] = true;
});
return next;
});
}, [pathname, navs]);
const renderCollapsedFlyout = (nav: NavItem) => {
const ParentButton = (
<SidebarMenuButton
aria-label={nav.title}
className="h-8 justify-center"
isActive={false}
size="sm"
>
{"url" in nav && nav.url ? (
<Link to={nav.url as string}>
{"icon" in nav && (nav as any).icon ? (
<Icon className="size-4" icon={(nav as any).icon} />
) : null}
</Link>
) : "icon" in nav && (nav as any).icon ? (
<Icon className="size-4" icon={(nav as any).icon} />
) : null}
</SidebarMenuButton>
);
if (!hasChildren(nav)) return ParentButton;
return (
<HoverCard closeDelay={200} openDelay={40}>
<HoverCardTrigger asChild>{ParentButton}</HoverCardTrigger>
<HoverCardContent
align="start"
avoidCollisions
className="z-[9999] w-64 p-0"
collisionPadding={8}
side="right"
sideOffset={10}
>
<div className="flex items-center gap-2 border-b px-3 py-2">
{"icon" in nav && (nav as any).icon ? (
<Icon className="size-4" icon={(nav as any).icon} />
) : null}
<span className="truncate font-medium text-muted-foreground text-xs">
{nav.title}
</span>
</div>
<ul className="p-1">
{nav.items?.map((item: any) => (
<li key={item.title}>
<Link
className={[
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm",
isActiveUrl(item.url)
? "bg-accent text-accent-foreground"
: "hover:bg-accent/60",
].join(" ")}
to={item.url}
>
{item.icon && <Icon className="size-4" icon={item.icon} />}
<span className="truncate">{item.title}</span>
</Link>
</li>
))}
</ul>
</HoverCardContent>
</HoverCard>
);
};
return (
<Sidebar className="border-r-0" collapsible="icon" {...props}>
<SidebarHeader className="p-2">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild className="h-10" size="sm">
<Link to="/">
<div className="flex aspect-square size-6 items-center justify-center rounded-lg">
<img
alt="logo"
className="size-full"
height={24}
src={site.site_logo || "/favicon.svg"}
width={24}
/>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold text-xs">
{site.site_name}
</span>
<span className="truncate text-xs opacity-70">
{site.site_desc}
</span>
</div>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="py-2">
<SidebarMenu>
{!isMobile && state === "collapsed"
? navs.map((nav) => (
<SidebarMenuItem className="mx-auto" key={nav.title}>
{renderCollapsedFlyout(nav)}
</SidebarMenuItem>
))
: navs.map((nav) => {
if (hasChildren(nav)) {
const isOpen = openGroups[nav.title] ?? false;
return (
<SidebarGroup className={cn("py-1")} key={nav.title}>
<SidebarMenuButton
className={cn(
"mb-2 flex h-8 w-full items-center justify-between hover:bg-accent/60 hover:text-accent-foreground"
)}
isActive={false}
onClick={() => handleToggleGroup(nav.title)}
size="sm"
style={{ fontWeight: 500 }}
tabIndex={0}
>
<span className="flex min-w-0 items-center gap-2">
{"icon" in nav && (nav as any).icon ? (
<Icon
className="size-4 shrink-0"
icon={(nav as any).icon}
/>
) : null}
<span className="truncate text-sm">{nav.title}</span>
</span>
<Icon
className={`ml-2 size-4 transition-transform ${isOpen ? "" : "-rotate-90"}`}
icon="mdi:chevron-down"
/>
</SidebarMenuButton>
{isOpen && (
<SidebarGroupContent className="px-4">
<SidebarMenu>
{nav.items?.map((item: any) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
className="h-8"
isActive={isActiveUrl(item.url)}
size="sm"
tooltip={item.title}
>
<Link to={item.url}>
{item.icon && (
<Icon
className="size-4"
icon={item.icon}
/>
)}
<span className="text-sm">
{item.title}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
)}
</SidebarGroup>
);
}
return (
<SidebarGroup className="py-1" key={nav.title}>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild={"url" in nav && !!(nav as any).url}
className="h-8"
isActive={
"url" in nav && (nav as any).url
? isActiveUrl((nav as any).url)
: false
}
size="sm"
tooltip={nav.title}
>
{"url" in nav && (nav as any).url ? (
<Link to={(nav as any).url}>
{"icon" in nav && (nav as any).icon ? (
<Icon
className="size-4"
icon={(nav as any).icon}
/>
) : null}
<span className="text-sm">{nav.title}</span>
</Link>
) : (
<>
{"icon" in nav && (nav as any).icon ? (
<Icon
className="size-4"
icon={(nav as any).icon}
/>
) : null}
<span className="text-sm">{nav.title}</span>
</>
)}
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
})}
</SidebarMenu>
</SidebarContent>
</Sidebar>
);
}
+315
View File
@@ -0,0 +1,315 @@
import { Button } from "@workspace/ui/components/button";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@workspace/ui/components/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@workspace/ui/components/popover";
import { Icon } from "@workspace/ui/composed/icon";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
interface TimezoneOption {
value: string;
label: string;
timezone: string;
}
function getCurrentTime(timezone: string): string {
try {
const now = new Date();
return now.toLocaleTimeString("en-US", {
timeZone: timezone,
hour12: false,
hour: "2-digit",
minute: "2-digit",
});
} catch {
return "--:--";
}
}
function getAllTimezones(locale = "en-US"): TimezoneOption[] {
try {
const timeZones = Intl.supportedValuesOf("timeZone");
const processed = timeZones
.map((tz) => {
try {
return {
value: tz,
label: tz,
timezone: getTimezoneOffset(tz),
};
} catch {
return {
value: tz,
label: tz,
timezone: "UTC+00:00",
};
}
})
.filter(Boolean)
.sort((a, b) => a.label.localeCompare(b.label, locale));
const hasUTC = processed.some((tz) => tz.value === "UTC");
if (!hasUTC) {
processed.unshift({
value: "UTC",
label: "UTC",
timezone: "UTC+00:00",
});
}
return processed;
} catch {
return [
{
value: "UTC",
label: "UTC",
timezone: "UTC+00:00",
},
];
}
}
function getServerTimezones(): string[] {
return ["UTC"];
}
function getRecommendedTimezones(): string[] {
try {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (browserTimezone.startsWith("Asia/")) {
return [
"Asia/Shanghai",
"Asia/Tokyo",
"Asia/Kolkata",
"Asia/Singapore",
"Asia/Seoul",
];
}
if (browserTimezone.startsWith("Europe/")) {
return [
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"Europe/Rome",
"Europe/Madrid",
];
}
if (browserTimezone.startsWith("America/")) {
return [
"America/New_York",
"America/Los_Angeles",
"America/Chicago",
"America/Denver",
"America/Toronto",
];
}
if (browserTimezone.startsWith("Australia/")) {
return [
"Australia/Sydney",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Brisbane",
];
}
return [
"America/New_York",
"Europe/London",
"Asia/Shanghai",
"Asia/Tokyo",
"Australia/Sydney",
];
} catch {
return [
"America/New_York",
"Europe/London",
"Asia/Shanghai",
"Asia/Tokyo",
"Australia/Sydney",
];
}
}
function getTimezoneOffset(timezone: string): string {
try {
const now = new Date();
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60_000);
const targetTime = new Date(
utc.toLocaleString("en-US", { timeZone: timezone })
);
const offset = (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? "+" : "-";
const hours = Math.floor(Math.abs(offset));
const minutes = Math.floor((Math.abs(offset) - hours) * 60);
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
} catch {
return "UTC+00:00";
}
}
export default function TimezoneSwitch() {
const { i18n } = useTranslation();
const locale = i18n.language;
const [timezone, setTimezone] = useState<string>("UTC");
const [open, setOpen] = useState(false);
const timezoneOptions = useMemo(() => getAllTimezones(locale), [locale]);
useEffect(() => {
const savedTimezone = localStorage.getItem("timezone");
if (savedTimezone) {
setTimezone(savedTimezone);
} else {
try {
const browserTimezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
setTimezone(browserTimezone);
localStorage.setItem("timezone", browserTimezone);
} catch {
setTimezone("UTC");
}
}
}, []);
const handleTimezoneChange = (newTimezone: string) => {
setTimezone(newTimezone);
localStorage.setItem("timezone", newTimezone);
setOpen(false);
window.dispatchEvent(
new CustomEvent("timezoneChanged", {
detail: { timezone: newTimezone },
})
);
};
const serverTimezones = timezoneOptions.filter(
(option) =>
getServerTimezones().includes(option.value) && option.value !== timezone
);
return (
<Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild>
<Button className="p-0" size="icon" variant="ghost">
<Icon className="!size-6" icon="flat-color-icons:overtime" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 p-0">
<Command>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandGroup heading="Current">
{timezoneOptions
.filter((option) => option.value === timezone)
.map((option) => (
<CommandItem
className="bg-primary/10"
key={option.value}
onSelect={() => handleTimezoneChange(option.value)}
value={`${option.label} ${option.value}`}
>
<div className="flex w-full items-center gap-3">
<div className="flex flex-1 flex-col">
<span className="font-medium">{option.value}</span>
<span className="text-muted-foreground text-xs">
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon className="h-4 w-4 opacity-100" icon="uil:check" />
</div>
</CommandItem>
))}
</CommandGroup>
{serverTimezones.length > 0 && (
<CommandGroup heading="Server">
{serverTimezones.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleTimezoneChange(option.value)}
value={`${option.label} ${option.value}`}
>
<div className="flex w-full items-center gap-3">
<div className="flex flex-1 flex-col">
<span className="font-medium">{option.value}</span>
<span className="text-muted-foreground text-xs">
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon className="h-4 w-4 opacity-0" icon="uil:check" />
</div>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Recommended">
{timezoneOptions
.filter(
(option) =>
getRecommendedTimezones().includes(option.value) &&
option.value !== timezone
)
.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleTimezoneChange(option.value)}
value={`${option.label} ${option.value}`}
>
<div className="flex w-full items-center gap-3">
<div className="flex flex-1 flex-col">
<span className="font-medium">{option.value}</span>
<span className="text-muted-foreground text-xs">
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon className="h-4 w-4 opacity-0" icon="uil:check" />
</div>
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading="All">
{timezoneOptions
.filter(
(option) =>
!(
getServerTimezones().includes(option.value) ||
getRecommendedTimezones().includes(option.value)
) && option.value !== timezone
)
.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleTimezoneChange(option.value)}
value={`${option.label} ${option.value}`}
>
<div className="flex w-full items-center gap-3">
<div className="flex flex-1 flex-col">
<span className="font-medium">{option.value}</span>
<span className="text-muted-foreground text-xs">
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon className="h-4 w-4 opacity-0" icon="uil:check" />
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
+73
View File
@@ -0,0 +1,73 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@workspace/ui/components/avatar";
import { Button } from "@workspace/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
import { Logout } from "@/utils/common";
export function UserNav() {
const { t } = useTranslation("auth");
const { user } = useGlobalStore();
if (user) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="default">
<Avatar className="size-8">
<AvatarImage alt={user?.avatar ?? ""} src={user?.avatar ?? ""} />
<AvatarFallback className="rounded-none bg-transparent">
{user?.auth_methods?.[0]?.auth_identifier
.toUpperCase()
.charAt(0)}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="font-medium text-sm leading-none">
{user?.auth_methods?.[0]?.auth_identifier}
</p>
{/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */}
</div>
</DropdownMenuLabel>
{/* <DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup> */}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={Logout}>
{t("logout", "Logout")}
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}