Files
claudemesh/apps/web/src/modules/user/user-navigation.tsx
Alejandro Gutiérrez d3163a5bff feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:19:32 +01:00

188 lines
6.1 KiB
TypeScript

"use client";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { memo } from "react";
import { hasAdminPermission } from "@turbostarter/auth";
import { useTranslation } from "@turbostarter/i18n";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@turbostarter/ui-web/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuPortal,
DropdownMenuGroup,
} from "@turbostarter/ui-web/dropdown-menu";
import { Icons } from "@turbostarter/ui-web/icons";
import { SidebarMenuButton, useSidebar } from "@turbostarter/ui-web/sidebar";
import { Skeleton } from "@turbostarter/ui-web/skeleton";
import { pathsConfig } from "~/config/paths";
import { authClient } from "~/lib/auth/client";
import { auth } from "~/modules/auth/lib/api";
import { ThemeControlsProvider } from "~/modules/common/theme";
import { TurboLink } from "~/modules/common/turbo-link";
import type { User } from "@turbostarter/auth";
interface UserNavigationProps {
readonly user: User;
}
export const UserNavigation = memo<UserNavigationProps>(({ user }) => {
const { t } = useTranslation(["common", "auth"]);
const router = useRouter();
const { isMobile, setOpenMobile } = useSidebar();
const { refetch } = authClient.useListOrganizations();
const signOut = useMutation({
...auth.mutations.signOut,
onSuccess: async () => {
await refetch();
router.replace(pathsConfig.index);
router.refresh();
},
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="size-8">
<AvatarImage src={user.image ?? undefined} alt={user.name} />
<AvatarFallback>
<Icons.UserRound className="w-5" />
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
{user.name && (
<span className="truncate font-medium">{user.name}</span>
)}
{user.email && (
<span className="truncate text-xs">{user.email}</span>
)}
</div>
<Icons.EllipsisVertical className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="flex items-center gap-2 font-normal">
<Avatar className="size-8">
<AvatarImage src={user.image ?? undefined} alt={user.name} />
<AvatarFallback>
<Icons.UserRound className="w-5" />
</AvatarFallback>
</Avatar>
<div className="flex w-full min-w-0 flex-col space-y-1">
{user.name && (
<p className="truncate text-sm leading-none font-medium">
{user.name}
</p>
)}
{user.email && (
<p className="text-muted-foreground truncate text-xs leading-none">
{user.email}
</p>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<TurboLink
href={pathsConfig.dashboard.user.index}
className="flex w-full cursor-pointer items-center gap-1.5"
onClick={() => setOpenMobile(false)}
>
<Icons.Home className="size-4" />
{t("dashboard")}
</TurboLink>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<TurboLink
href={pathsConfig.dashboard.user.settings.index}
className="flex w-full cursor-pointer items-center gap-1.5"
onClick={() => setOpenMobile(false)}
>
<Icons.Settings className="size-4" />
{t("settings")}
</TurboLink>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<ThemeControlsProvider>
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div className="flex w-full cursor-pointer items-center gap-1.5">
<Icons.Sun className="size-4 dark:hidden" />
<Icons.Moon className="hidden size-4 dark:block" />
{t("theme.title")}
<div className="bg-primary ml-auto size-3 rounded-full"></div>
</div>
</DropdownMenuItem>
</ThemeControlsProvider>
</DropdownMenuGroup>
{hasAdminPermission(user) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<TurboLink
href={pathsConfig.admin.index}
className="flex w-full cursor-pointer items-center gap-1.5"
>
<Icons.ShieldUser className="size-4" />
{t("common:admin")}
</TurboLink>
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="cursor-pointer">
<button
className="flex w-full items-center gap-1.5"
onClick={() => signOut.mutate(undefined)}
>
<Icons.LogOut className="size-4" />
{t("logout.cta")}
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
);
});
export const UserNavigationSkeleton = () => {
return <Skeleton className="size-10 rounded-full" />;
};
UserNavigation.displayName = "UserNavigation";