s
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "@/app/digital/i18n/navigation";
|
||||
import { Link, useTranslation } from "@/app/digital/i18n/navigation";
|
||||
import { pbLogout } from "@/app/digital/lib/pocketbase";
|
||||
import { useCrossSiteUrls } from "@/app/digital/lib/useCrossSiteUrls";
|
||||
import DigitalThemeIcon, { type IconName } from "./DigitalThemeIcon";
|
||||
|
||||
function getAvatarLetter(email: string): string {
|
||||
const local = email.split("@")[0];
|
||||
@@ -17,12 +19,29 @@ interface UserAvatarProps {
|
||||
onRefreshVip?: () => void;
|
||||
}
|
||||
|
||||
/** 圆形头像 + VIP 标识 + 点击展开退出按钮,供 digital / meetup / vip 三站复用 */
|
||||
type MenuLink = {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
emoji: string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
/** 圆形头像 + 账户菜单,供 digital / meetup / vip 三站复用 */
|
||||
export default function UserAvatar({ email, vip, onLogout }: UserAvatarProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { vipUrl } = useCrossSiteUrls();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const menuLinks: MenuLink[] = [
|
||||
{ href: "/profile", label: t("profile"), icon: "user", emoji: "👤" },
|
||||
{ href: "/course", label: t("course"), icon: "course", emoji: "🎓" },
|
||||
{ href: "/ebook", label: t("ebook"), icon: "book", emoji: "📚" },
|
||||
{ href: "/app", label: t("app"), icon: "phone", emoji: "📱" },
|
||||
{ href: vipUrl, label: t("community"), icon: "globe", emoji: "🌐", external: true },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
@@ -34,6 +53,9 @@ export default function UserAvatar({ email, vip, onLogout }: UserAvatarProps) {
|
||||
const handleLogout = async () => {
|
||||
await pbLogout();
|
||||
setOpen(false);
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:updated"));
|
||||
}
|
||||
onLogout?.();
|
||||
};
|
||||
|
||||
@@ -42,30 +64,67 @@ export default function UserAvatar({ email, vip, onLogout }: UserAvatarProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-sky-500 text-sm font-semibold text-white shadow-sm ring-2 ring-white dark:ring-slate-900 hover:bg-sky-600"
|
||||
aria-label={t("logout")}
|
||||
className="dn-avatar-button relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-sky-500 text-sm font-semibold text-white shadow-sm ring-2 ring-white transition hover:bg-sky-600 dark:ring-slate-900"
|
||||
aria-label={t("accountMenu")}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{getAvatarLetter(email)}
|
||||
{vip && (
|
||||
<span
|
||||
className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-amber-400 text-[10px] text-amber-900"
|
||||
className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-400 px-1 text-[9px] font-bold text-amber-900"
|
||||
title="VIP"
|
||||
>
|
||||
👑
|
||||
VIP
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 min-w-[120px] rounded-lg border border-slate-200 bg-white py-1 shadow-lg dark:border-slate-700 dark:bg-slate-800">
|
||||
{vip && (
|
||||
<div className="border-b border-slate-100 px-4 py-2 text-xs font-medium text-amber-600 dark:border-slate-700 dark:text-amber-400">
|
||||
👑 VIP
|
||||
<div className="dn-profile-menu absolute right-0 top-full z-50 mt-2 w-[280px] overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="border-b border-slate-100 px-4 py-3 dark:border-slate-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sky-500 text-sm font-bold text-white">
|
||||
{getAvatarLetter(email)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-slate-900 dark:text-white">{email}</p>
|
||||
<p className="mt-0.5 text-xs font-medium text-slate-500 dark:text-slate-400">
|
||||
{vip ? t("vipMember") : t("standardMember")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-2 py-2">
|
||||
{menuLinks.map((item) =>
|
||||
item.external ? (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setOpen(false)}
|
||||
className="dn-profile-menu-item flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||
>
|
||||
<DigitalThemeIcon name={item.icon} emoji={item.emoji} className="h-4 w-4 shrink-0 text-base" />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="dn-profile-menu-item flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||
>
|
||||
<DigitalThemeIcon name={item.icon} emoji={item.emoji} className="h-4 w-4 shrink-0 text-base" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||
className="dn-profile-menu-item flex w-full items-center gap-3 border-t border-slate-100 px-5 py-3 text-left text-sm font-semibold text-red-600 transition hover:bg-red-50 dark:border-slate-700 dark:text-red-300 dark:hover:bg-red-950/30"
|
||||
>
|
||||
<DigitalThemeIcon name="home" emoji="↩" className="h-4 w-4 shrink-0 text-base" />
|
||||
{t("logout")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user