2025-09-06 17:19:19 +08:00

396 lines
14 KiB
TypeScript

"use client";
import "../pages/style/top-bar.css";
import { Button } from "@/components/ui/button";
import { GradientText } from "@/components/ui/gradient-text";
import { useTheme } from "next-themes";
import {
Sun,
Moon,
User,
Sparkles,
LogOut,
PanelsLeftBottom,
Bell,
} from "lucide-react";
import { motion } from "framer-motion";
import ReactDOM from "react-dom";
import { useRouter, usePathname } from "next/navigation";
import React, { useRef, useEffect, useLayoutEffect, useState } from "react";
import { logoutUser } from "@/lib/auth";
import {
createPortalSession,
redirectToPortal,
getUserSubscriptionInfo,
} from "@/lib/stripe";
import UserCard from "@/components/common/userCard";
interface User {
id: string;
name: string;
email: string;
avatar: string;
username: string;
plan_name?: string;
}
export function TopBar({ collapsed }: { collapsed: boolean }) {
const router = useRouter();
const [isOpen, setIsOpen] = React.useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [currentUser, setCurrentUser] = useState<User>(
JSON.parse(localStorage.getItem("currentUser") || "{}")
);
const pathname = usePathname();
const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false);
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState<string>("");
const [credits, setCredits] = useState<number>(0);
const [isLoadingSubscription, setIsLoadingSubscription] = useState(false);
// 获取用户订阅信息
const fetchSubscriptionInfo = async () => {
if (!currentUser?.id) return;
setIsLoadingSubscription(true);
try {
const response = await getUserSubscriptionInfo(String(currentUser.id));
if (response.successful && response.data) {
const status = response.data.subscription_status;
setSubscriptionStatus(status);
setCredits(response.data.credits);
// 更新 currentUser 的 plan_name
setCurrentUser((prev) => ({
...prev,
plan_name: response.data.plan_name, // HACK
}));
}
} catch (error) {
console.error("获取订阅信息失败:", error);
} finally {
setIsLoadingSubscription(false);
}
};
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
setIsLogin(true);
} else {
setIsLogin(false);
}
});
useLayoutEffect(() => {
console.log("Setting mounted state");
setMounted(true);
return () => console.log("Cleanup mounted effect");
}, []);
// 处理订阅管理
const handleManageSubscription = async () => {
if (!currentUser?.id) {
console.error("用户未登录");
return;
}
setIsManagingSubscription(true);
try {
const response = await createPortalSession({
user_id: String(currentUser.id),
return_url: window.location.origin + "/dashboard",
});
if (response.successful && response.data?.portal_url) {
redirectToPortal(response.data.portal_url);
} else {
console.log("cannot open the manage subscription");
return;
}
} catch (error) {
console.log("cannot open the manage subscription");
return;
} finally {
setIsManagingSubscription(false);
}
};
// 处理点击事件
useEffect(() => {
if (!isOpen) return;
let isClickStartedInside = false;
const handleMouseDown = (event: MouseEvent) => {
const target = event.target as Node;
isClickStartedInside = !!(
menuRef.current?.contains(target) || buttonRef.current?.contains(target)
);
};
const handleMouseUp = (event: MouseEvent) => {
const target = event.target as Node;
const isClickEndedInside = !!(
menuRef.current?.contains(target) || buttonRef.current?.contains(target)
);
// 只有当点击开始和结束都在外部时才关闭
if (!isClickStartedInside && !isClickEndedInside) {
setIsOpen(false);
}
isClickStartedInside = false;
};
// 在冒泡阶段处理事件
document.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isOpen]);
const handleAnimationEnd = (event: React.AnimationEvent<HTMLDivElement>) => {
const element = event.currentTarget;
element.classList.remove("on");
};
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
const element = event.currentTarget;
element.classList.add("on");
};
return (
<div
className="fixed right-0 top-0 h-16 header z-[999]"
style={{
isolation: "isolate",
left: pathname === "/" ? "0" : (collapsed ? "2.5rem" : "16rem")
}}
>
<div className="h-full flex items-center justify-between pr-6 pl-4">
<div className="flex items-center space-x-4">
<div
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
onClick={() => router.push("/")}
onMouseEnter={handleMouseEnter}
onAnimationEnd={handleAnimationEnd}
>
<span className="translate">
<span>
<h1 className="logo text-2xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</h1>
</span>
<span>
<h1 className="logo text-2xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</h1>
</span>
</span>
{/* beta标签 */}
<div className="relative transform translate-y-[-1px]">
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
Beta
</span>
</div>
</div>
</div>
{isLogin ? (
<div className="flex items-center space-x-2">
{/* Pricing Link */}
{pathname === "/" ? (
<div
data-alt="go-started-button"
className="z-100 pointer-events-auto bg-white text-black rounded-full cursor-pointer transition-opacity opacity-100 hover:opacity-80 font-medium
/* 移动端适配 */
px-2 py-1.5 text-xs
/* 平板及以上适配 */
sm:px-4 sm:py-2 sm:text-sm"
onClick={() => router.push("/movies")}
>
Go Started
</div>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => {
localStorage.setItem("callBackUrl", pathname);
window.open("/pricing", "_blank");
}}
className="text-gray-300 hover:text-white"
>
Upgrade
</Button>
)}
{/* Notifications */}
{/* <Button variant="ghost" size="sm" onClick={() => window.msg.error('Loading...')}>
<Bell className="h-4 w-4" />
</Button> */}
{/* Theme Toggle */}
{/* <Button
variant="ghost"
size="sm"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button> */}
{/* User Menu */}
<div className="relative" style={{ isolation: "isolate" }}>
<Button
ref={buttonRef}
variant="ghost"
size="sm"
onClick={() => {
console.log("Button clicked, current isOpen:", isOpen);
if (!isOpen) {
// 每次打开菜单时重新获取订阅信息
fetchSubscriptionInfo();
}
setIsOpen(!isOpen);
}}
data-alt="user-menu-trigger"
style={{
background: "unset !important",
padding: "unset !important",
}}
>
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
{currentUser.username ? currentUser.username.charAt(0) : "MF"}
</div>
</Button>
{mounted && isOpen
? ReactDOM.createPortal(
<motion.div
ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
style={{
position: "fixed",
top: "4rem",
right: "1rem",
zIndex: 9999,
}}
className="overflow-hidden rounded-xl"
data-alt="user-menu-dropdown"
onClick={(e) => e.stopPropagation()}
>
<UserCard plan_name={currentUser.plan_name}>
<div className="relative z-[2] w-full h-full flex flex-col text-white p-3">
{/* 顶部用户信息 */}
<div className="flex items-center space-x-3 mb-3">
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
{currentUser.username
? currentUser.username.charAt(0)
: "MF"}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-white truncate">
{currentUser.name || currentUser.username}
</h3>
<p className="text-xs text-gray-300 truncate">
{currentUser.email}
</p>
</div>
</div>
{/* AI 积分 */}
<div className="flex items-center justify-center space-x-3 mb-4">
<div className="p-2 rounded-full bg-white/10 backdrop-blur-sm">
<Sparkles className="h-5 w-5 text-white" />
</div>
<span className="text-white text-base font-semibold">
{isLoadingSubscription
? "Loading..."
: `${credits} credits`}
</span>
</div>
{/* 操作按钮区域 */}
<div className="flex-1 flex flex-row justify-center items-end">
<button
className="flex-1 bg-transparent border border-white/30 text-white text-xs py-0.5 h-6 rounded hover:bg-white/10 transition-colors"
onClick={() => {
window.open("/pricing", "_blank");
}}
>
Upgrade
</button>
{currentUser.plan_name !== "none" && (
<button
className="flex-1 bg-transparent border border-gray-400/30 text-gray-300 text-xs py-0.5 h-6 rounded hover:bg-gray-400/10 transition-colors disabled:opacity-50"
onClick={handleManageSubscription}
disabled={isManagingSubscription}
>
Manage
</button>
)}
<button
className="flex-1 bg-transparent border border-red-400/50 text-red-300 text-xs py-0.5 h-6 rounded hover:bg-red-400/10 transition-colors"
onClick={() => logoutUser()}
>
Logout
</button>
</div>
</div>
</UserCard>
</motion.div>,
document.body
)
: null}
</div>
</div>
) : (
<div className="flex items-center space-x-2 sm:space-x-4">
<div
data-alt="login-button"
className="z-100 pointer-events-auto text-gray-300 hover:text-white cursor-pointer rounded transition-colors
/* 移动端适配 */
px-2 py-1.5 text-xs
/* 平板及以上适配 */
sm:px-3 sm:py-2 sm:text-sm"
onClick={() => router.push("/signup")}
>
Sign Up
</div>
<div
data-alt="go-started-button"
className="z-100 pointer-events-auto bg-white text-black rounded-full cursor-pointer transition-opacity opacity-100 hover:opacity-80 font-medium
/* 移动端适配 */
px-2 py-1.5 text-xs
/* 平板及以上适配 */
sm:px-4 sm:py-2 sm:text-sm"
onClick={() => router.push("/movies")}
>
Go Started
</div>
</div>
)}
</div>
</div>
);
}