99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useRef } from "react";
|
||
import { Link, useTranslation } from "@/i18n/navigation";
|
||
import { pbLogout } from "@/app/lib/pocketbase";
|
||
|
||
function getAvatarLetter(email: string): string {
|
||
const local = email.split("@")[0];
|
||
if (!local) return "?";
|
||
return local.slice(0, 1).toUpperCase();
|
||
}
|
||
|
||
interface UserAvatarProps {
|
||
email: string;
|
||
isVip?: boolean;
|
||
onLogout?: () => void;
|
||
}
|
||
|
||
/** 圆形头像 + 账户菜单 */
|
||
export default function UserAvatar({ email, isVip = false, onLogout }: UserAvatarProps) {
|
||
const { t } = useTranslation("menu");
|
||
const [open, setOpen] = useState(false);
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||
};
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||
}, []);
|
||
|
||
const handleLogout = async () => {
|
||
await pbLogout();
|
||
setOpen(false);
|
||
onLogout?.();
|
||
};
|
||
|
||
return (
|
||
<div className="relative" ref={ref}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpen((o) => !o)}
|
||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#ff4d4f] text-sm font-semibold text-white shadow-sm ring-2 ring-white transition hover:bg-[#ff7a45] dark:ring-gray-900"
|
||
aria-label={t("yourProfile")}
|
||
aria-expanded={open}
|
||
>
|
||
{getAvatarLetter(email)}
|
||
</button>
|
||
{isVip && (
|
||
<span className="absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] font-bold text-white shadow-sm">
|
||
VIP
|
||
</span>
|
||
)}
|
||
{open && (
|
||
<div className="absolute right-0 top-full z-50 mt-2 w-[260px] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
|
||
<div className="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
|
||
<p className="truncate text-sm font-semibold text-gray-900 dark:text-white">{email}</p>
|
||
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{isVip ? "VIP" : t("yourProfile")}</p>
|
||
</div>
|
||
<div className="px-2 py-2">
|
||
<Link
|
||
href="/digital/profile"
|
||
onClick={() => setOpen(false)}
|
||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||
>
|
||
<span className="w-5 text-center">👤</span>
|
||
<span>{t("yourProfile")}</span>
|
||
</Link>
|
||
<Link
|
||
href="/settings/notifications"
|
||
onClick={() => setOpen(false)}
|
||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||
>
|
||
<span className="w-5 text-center">⚙️</span>
|
||
<span>{t("settings")}</span>
|
||
</Link>
|
||
<Link
|
||
href="/pricing"
|
||
onClick={() => setOpen(false)}
|
||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||
>
|
||
<span className="w-5 text-center">👑</span>
|
||
<span>{t("pricing")}</span>
|
||
</Link>
|
||
</div>
|
||
<button
|
||
onClick={handleLogout}
|
||
className="flex w-full items-center gap-2 border-t border-gray-100 px-5 py-3 text-left text-sm font-semibold text-red-600 transition hover:bg-red-50 dark:border-gray-700 dark:text-red-300 dark:hover:bg-red-950/30"
|
||
>
|
||
<span className="w-5 text-center">🚪</span>
|
||
{t("logout")}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|