126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
/**
|
|
* 顶栏用户菜单:接 `auth.me` / `auth.logout`,未登录显示登录/注册入口。
|
|
*/
|
|
|
|
import type { Locale } from "@app/i18n";
|
|
import { t } from "@app/i18n";
|
|
import {
|
|
Avatar,
|
|
AvatarFallback,
|
|
AvatarImage,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
toast,
|
|
} from "@app/ui";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { createTRPCReactClient, trpc } from "../lib/trpc";
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
|
|
});
|
|
const trpcClient = createTRPCReactClient();
|
|
|
|
export interface UserMenuProps {
|
|
locale?: Locale | undefined;
|
|
name?: string | undefined;
|
|
email?: string | undefined;
|
|
avatarUrl?: string | undefined;
|
|
}
|
|
|
|
function home(locale: Locale): string {
|
|
return locale === "zh-CN" ? "/zh-CN" : "/en";
|
|
}
|
|
|
|
export default function UserMenu({ locale = "zh-CN", avatarUrl }: UserMenuProps) {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
<UserMenuInner locale={locale} avatarUrl={avatarUrl} />
|
|
</trpc.Provider>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
function UserMenuInner({ locale, avatarUrl }: { locale: Locale; avatarUrl?: string | undefined }) {
|
|
const me = trpc.auth.me.useQuery();
|
|
const logout = trpc.auth.logout.useMutation({
|
|
onSuccess: () => {
|
|
toast.success(t(locale, "auth.msg.logoutOk"));
|
|
queryClient.clear();
|
|
window.location.reload();
|
|
},
|
|
onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")),
|
|
});
|
|
|
|
const base = home(locale);
|
|
const user = me.data?.user ?? null;
|
|
|
|
if (me.isPending) {
|
|
return <span className="u-mono hidden text-[11px] sm:block opacity-60">…</span>;
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<span className="flex items-center gap-2">
|
|
<a className="u-linelink" href={`${base}/auth/login`}>
|
|
{t(locale, "auth.menu.login")}
|
|
</a>
|
|
<a className="u-linelink hidden sm:inline" href={`${base}/auth/register`}>
|
|
{t(locale, "auth.menu.register")}
|
|
</a>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const name = user.name;
|
|
const initials = name.slice(0, 1);
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button type="button" className="u-btn u-btn--ghost px-2">
|
|
<Avatar className="size-6">
|
|
{avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
|
|
<AvatarFallback
|
|
className="u-mono text-[10px]"
|
|
style={{ background: "var(--sink)", color: "var(--edge-as-text)" }}
|
|
>
|
|
{initials}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="u-mono hidden text-[11px] sm:block">{name}</span>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-sm font-medium">{name}</span>
|
|
<span className="u-mono text-[11px] opacity-70">{user.email}</span>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem asChild>
|
|
<a href={`${base}/dashboard`}>{t(locale, "auth.menu.dashboard")}</a>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<a href={`${base}/settings`}>{t(locale, "auth.menu.settings")}</a>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onSelect={(e) => {
|
|
e.preventDefault();
|
|
logout.mutate();
|
|
}}
|
|
disabled={logout.isPending}
|
|
>
|
|
{t(locale, "auth.menu.logout")}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|