This commit is contained in:
eric
2026-03-13 04:41:51 -05:00
parent 37d0883ce9
commit a193fe4d45
22 changed files with 1119 additions and 468 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation";
import { getStoredUserEmail } from "@/app/lib/pocketbase";
import { useCrossSiteUrls } from "@/app/lib/useCrossSiteUrls";
@@ -18,6 +18,7 @@ export default function Header() {
const [scrolled, setScrolled] = useState(false);
const [authOpen, setAuthOpen] = useState(false);
const [userEmail, setUserEmail] = useState<string | null>(null);
const [vip, setVip] = useState(false);
const { vipUrl } = useCrossSiteUrls();
const navLinks = [
{ labelKey: "roadmap" as const, href: "#roadmap" },
@@ -26,27 +27,42 @@ export default function Header() {
{ labelKey: "resources" as const, href: "#resources" },
];
useEffect(() => {
let mounted = true;
(async () => {
try {
const res = await fetch("/api/auth/me", { credentials: "include" });
const data = await res.json();
if (!mounted) return;
if (data?.user && data?.token) {
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(data.token, { id: data.user.id, email: data.user.email });
setUserEmail(data.user.email);
return;
}
} catch {
/* ignore */
const fetchAuthAndVip = useCallback(async () => {
try {
const meRes = await fetch("/api/auth/me", { credentials: "include" });
const meData = await meRes.json();
if (meData?.user && meData?.token) {
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(meData.token, { id: meData.user.id, email: meData.user.email });
setUserEmail(meData.user.email);
// 用邮箱查 VIP比 cookie userId 更可靠
const vipRes = await fetch(`/api/vip/check?email=${encodeURIComponent(meData.user.email)}`, { credentials: "include" });
const vipData = await vipRes.json();
setVip(vipData?.vip === true);
} else {
setUserEmail(getStoredUserEmail());
setVip(false);
}
if (mounted) setUserEmail(getStoredUserEmail());
})();
return () => { mounted = false; };
} catch {
/* ignore */
}
}, []);
useEffect(() => {
fetchAuthAndVip();
}, [fetchAuthAndVip]);
useEffect(() => {
const onVipUpdated = () => fetchAuthAndVip();
const onAuthSuccess = () => fetchAuthAndVip();
window.addEventListener("vip:updated", onVipUpdated);
window.addEventListener("auth:success", onAuthSuccess);
return () => {
window.removeEventListener("vip:updated", onVipUpdated);
window.removeEventListener("auth:success", onAuthSuccess);
};
}, [fetchAuthAndVip]);
return (
<header
className={`fixed top-0 left-0 right-0 z-50 w-full max-w-[100vw] transition-all duration-300 ${
@@ -78,7 +94,7 @@ export default function Header() {
<div className="flex shrink-0 items-center gap-1 sm:gap-3">
{userEmail ? (
<UserAvatar email={userEmail} onLogout={() => setUserEmail(null)} />
<UserAvatar email={userEmail} vip={vip} onLogout={() => { setUserEmail(null); setVip(false); router.replace("/"); }} />
) : (
<button
onClick={() => setAuthOpen(true)}
@@ -148,7 +164,7 @@ export default function Header() {
</button>
{userEmail ? (
<div className="mt-2 flex items-center gap-3 rounded-lg border border-slate-100 bg-slate-50 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
<UserAvatar email={userEmail} onLogout={() => { setUserEmail(null); setMobileOpen(false); }} />
<UserAvatar email={userEmail} vip={vip} onLogout={() => { setUserEmail(null); setVip(false); setMobileOpen(false); router.replace("/"); }} />
<span className="truncate text-sm text-slate-600 dark:text-slate-400">{userEmail}</span>
</div>
) : (
@@ -176,7 +192,7 @@ export default function Header() {
<AuthModal
isOpen={authOpen}
onClose={() => setAuthOpen(false)}
onSuccess={(email) => setUserEmail(email)}
onSuccess={() => fetchAuthAndVip()}
/>
</header>
);