Files
tool-evaluator/artifacts/toolrate/src/components/user-menu.tsx
T
2026-08-02 14:55:18 +02:00

137 lines
4.3 KiB
TypeScript

import { useState } from "react";
import { useLocation } from "wouter";
import { useTranslation } from "react-i18next";
import { useAuth } from "@/hooks/use-auth";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Bookmark, LogIn, LogOut, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
function initials(name?: string | null): string {
if (!name) return "?";
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
return parts
.slice(0, 2)
.map((p) => p[0]?.toUpperCase() ?? "")
.join("");
}
export function UserMenu() {
const [location, navigate] = useLocation();
const { t } = useTranslation();
const { user, isLoading, isAuthenticated, isAdmin, tier, hasFeature, login, logout } = useAuth();
const [open, setOpen] = useState(false);
const showWatchlist = hasFeature("watchlist");
const showTrash = hasFeature("trash");
const displayName = user?.name || user?.preferredUsername || "User";
if (isLoading) {
return (
<div className="flex items-center gap-3 px-3 py-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-24" />
</div>
);
}
if (!isAuthenticated || !user) {
return (
<Button
variant="outline"
size="sm"
className="w-full gap-2"
onClick={() => login(location)}
data-testid="button-login"
>
<LogIn className="w-4 h-4" />
{t("auth.signIn")}
</Button>
);
}
function go(path: string) {
setOpen(false);
navigate(path);
}
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-3 px-3 py-2 h-auto"
data-testid="button-user-menu"
>
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-primary/15 text-primary text-xs font-semibold">
{initials(displayName)}
</AvatarFallback>
</Avatar>
<span className="min-w-0 flex-1 text-left">
<span className="block text-sm font-medium truncate">{displayName}</span>
<span className="block text-[11px] text-muted-foreground truncate">
{user.email ?? tier}
</span>
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
<DropdownMenuLabel className="flex items-center justify-between gap-2 font-normal">
<span className="min-w-0">
<span className="block text-sm font-medium text-foreground truncate">{displayName}</span>
{user.email && <span className="block text-xs text-muted-foreground truncate">{user.email}</span>}
</span>
<Badge
variant="outline"
className={cn(
"shrink-0 text-[10px] uppercase tracking-wider",
isAdmin && "border-primary text-primary",
)}
>
{isAdmin ? "admin" : tier}
</Badge>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{(showWatchlist || showTrash) && (
<>
{showWatchlist && (
<DropdownMenuItem onSelect={() => go("/watchlist")}>
<Bookmark className="mr-2 h-4 w-4" />
{t("nav.watchlist")}
</DropdownMenuItem>
)}
{showTrash && (
<DropdownMenuItem onSelect={() => go("/trash")}>
<Trash2 className="mr-2 h-4 w-4" />
{t("nav.trash")}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={logout}
data-testid="button-logout"
>
<LogOut className="mr-2 h-4 w-4" />
{t("auth.signOut")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}