108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import Link from "next/link";
|
|
import { ThemeToggle } from "@/app/components/ThemeToggle";
|
|
import { Copyable } from "./Copyable";
|
|
import { pbLogout } from "@/app/lib/pocketbase";
|
|
import styles from "./vip.module.css";
|
|
|
|
function getAvatarLetter(email: string): string {
|
|
const local = email.split("@")[0];
|
|
if (!local) return "?";
|
|
return local.slice(0, 1).toUpperCase();
|
|
}
|
|
|
|
type VipHeaderProps = {
|
|
isLoggedIn: boolean;
|
|
userName?: string | null;
|
|
userEmail?: string | null;
|
|
wechatNick?: string | null;
|
|
wechatAvatar?: string | null;
|
|
onLogout?: () => void;
|
|
};
|
|
|
|
export function VipHeader({
|
|
isLoggedIn,
|
|
userName,
|
|
userEmail,
|
|
wechatNick,
|
|
wechatAvatar,
|
|
onLogout,
|
|
}: VipHeaderProps) {
|
|
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?.();
|
|
};
|
|
|
|
const hasWechatProfile = !!(wechatNick || wechatAvatar);
|
|
const showUser = isLoggedIn || !!userEmail || hasWechatProfile;
|
|
const displayName = wechatNick || userName || userEmail || "会员";
|
|
|
|
return (
|
|
<header className={styles.header}>
|
|
<Link href="/" className={styles.logo}>
|
|
🌍 异度星球
|
|
</Link>
|
|
<nav className={styles.nav}>
|
|
<ThemeToggle />
|
|
{showUser ? (
|
|
<div className={styles.userBadge} style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
<div className="relative" ref={ref}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((o) => !o)}
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-amber-500 text-sm font-semibold text-white shadow-sm hover:bg-amber-600"
|
|
aria-label="用户菜单"
|
|
>
|
|
{wechatAvatar ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img src={wechatAvatar} alt={displayName} className="h-full w-full object-cover" />
|
|
) : (
|
|
getAvatarLetter(displayName)
|
|
)}
|
|
</button>
|
|
{isLoggedIn && (
|
|
<span
|
|
className={styles.vipTag}
|
|
title="VIP 会员"
|
|
>
|
|
VIP
|
|
</span>
|
|
)}
|
|
{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">
|
|
<span className="block px-4 py-2 text-sm text-slate-600 dark:text-slate-400 truncate max-w-[160px]">
|
|
<Copyable text={displayName}>{displayName}</Copyable>
|
|
</span>
|
|
{isLoggedIn && onLogout && (
|
|
<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"
|
|
>
|
|
退出
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<span className="hidden sm:inline max-w-[120px] truncate text-sm"><Copyable text={displayName}>{displayName}</Copyable></span>
|
|
</div>
|
|
) : null}
|
|
</nav>
|
|
</header>
|
|
);
|
|
}
|